Merge branch 'main' into grcp-server-reflect

This commit is contained in:
Livio Spring 2023-05-16 10:51:32 +02:00 committed by GitHub
commit 383e68b819
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2333 additions and 296 deletions

View File

@ -7,6 +7,7 @@
/k8s/
/node_modules/
/console/src/app/proto/generated/
/console/.angular
/console/tmp/
.releaserc.js
changelog.config.js
@ -18,3 +19,4 @@ pkg/grpc/*/*.pb.*
pkg/grpc/*/*.swagger.json
.goreleaser.yaml
.artifacts/
.vscode

View File

@ -80,7 +80,7 @@ jobs:
name: go-codecov
- name: Bump Chart Version
uses: peter-evans/repository-dispatch@v2
if: steps.semantic.outputs.new_release_published == 'true' && github.ref == 'refs/heads/main'
if: steps.semantic.outputs.new_release_published == 'true' && github.ref == 'refs/heads/next'
with:
token: ${{ steps.generate-token.outputs.token }}
repository: zitadel/zitadel-charts

View File

@ -338,6 +338,7 @@ Please refer to the [README](./docs/README.md) for more information and local te
- **Code with variables**: Make sure that code snippets can be used by setting environment variables, instead of manually replacing a placeholder.
- **Embedded files**: When embedding mdx files, make sure the template ist prefixed by "_" (lowdash). The content will be rendered inside the parent page, but is not accessible individually (eg, by search).
- **Don't repeat yourself**: When using the same content in multiple places, save and manage the content as separate file and make use of embedded files to import it into other docs pages.
- **Embedded code**: You can embed code snippets from a repository. See the [plugin](https://github.com/saucelabs/docusaurus-theme-github-codeblock#usage) for usage.
### Docs Pull Request

View File

@ -96,4 +96,16 @@ protoc \
--validate_out=lang=go:${GOPATH}/src \
${PROTO_PATH}/session/v2alpha/session_service.proto
protoc \
-I=/proto/include \
--grpc-gateway_out ${GOPATH}/src \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt allow_delete_body=true \
--openapiv2_out ${OPENAPI_PATH} \
--openapiv2_opt logtostderr=true \
--openapiv2_opt allow_delete_body=true \
--zitadel_out=${GOPATH}/src \
--validate_out=lang=go:${GOPATH}/src \
${PROTO_PATH}/settings/v2alpha/settings_service.proto
echo "done generating grpc"

View File

@ -267,6 +267,7 @@ Console:
LongCache:
MaxAge: 12h
SharedMaxAge: 168h #7d
InstanceManagementURL: ""
Notification:
Repository:

View File

@ -13,11 +13,11 @@ import (
)
var (
//go:embed 10_create_temp_table.sql
//go:embed 10/10_create_temp_table.sql
correctCreationDate10CreateTable string
//go:embed 10_fill_table.sql
//go:embed 10/10_fill_table.sql
correctCreationDate10FillTable string
//go:embed 10_update.sql
//go:embed 10/10_update.sql
correctCreationDate10Update string
)

32
cmd/setup/11.go Normal file
View File

@ -0,0 +1,32 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
)
var (
//go:embed 11.sql
addEventCreatedAt string
)
type AddEventCreatedAt struct {
step10 *CorrectCreationDate
dbClient *database.DB
}
func (mig *AddEventCreatedAt) Execute(ctx context.Context) error {
// execute step 10 again because events created after the first execution of step 10
// could still have the wrong ordering of sequences and creation date
if err := mig.step10.Execute(ctx); err != nil {
return err
}
_, err := mig.dbClient.ExecContext(ctx, addEventCreatedAt)
return err
}
func (mig *AddEventCreatedAt) String() string {
return "11_event_created_at"
}

15
cmd/setup/11.sql Normal file
View File

@ -0,0 +1,15 @@
BEGIN;
-- create table with empty created_at
ALTER TABLE eventstore.events ADD COLUMN created_at TIMESTAMPTZ DEFAULT NULL;
COMMIT;
BEGIN;
-- backfill created_at
UPDATE eventstore.events SET created_at = creation_date WHERE created_at IS NULL;
COMMIT;
BEGIN;
-- set column rules
ALTER TABLE eventstore.events ALTER COLUMN created_at SET DEFAULT clock_timestamp();
ALTER TABLE eventstore.events ALTER COLUMN created_at SET NOT NULL;
COMMIT;

View File

@ -66,6 +66,7 @@ type Steps struct {
s8AuthTokens *AuthTokenIndexes
s9EventstoreIndexes2 *EventstoreIndexesNew
CorrectCreationDate *CorrectCreationDate
s11AddEventCreatedAt *AddEventCreatedAt
}
type encryptionKeyConfig struct {

View File

@ -91,6 +91,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s8AuthTokens = &AuthTokenIndexes{dbClient: dbClient}
steps.s9EventstoreIndexes2 = New09(dbClient)
steps.CorrectCreationDate.dbClient = dbClient
steps.s11AddEventCreatedAt = &AddEventCreatedAt{dbClient: dbClient, step10: steps.CorrectCreationDate}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@ -128,6 +129,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
logging.OnError(err).Fatal("unable to migrate step 9")
err = migration.Migrate(ctx, eventstoreClient, steps.CorrectCreationDate)
logging.OnError(err).Fatal("unable to migrate step 10")
err = migration.Migrate(ctx, eventstoreClient, steps.s11AddEventCreatedAt)
logging.OnError(err).Fatal("unable to migrate step 11")
for _, repeatableStep := range repeatableSteps {
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)

View File

@ -5,7 +5,7 @@ import (
"crypto/tls"
_ "embed"
"fmt"
"net"
"math"
"net/http"
"os"
"os/signal"
@ -21,7 +21,6 @@ import (
"github.com/zitadel/saml/pkg/provider"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/sys/unix"
"github.com/zitadel/zitadel/cmd/key"
cmd_tls "github.com/zitadel/zitadel/cmd/tls"
@ -34,6 +33,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/auth"
"github.com/zitadel/zitadel/internal/api/grpc/management"
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
"github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
"github.com/zitadel/zitadel/internal/api/grpc/system"
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
http_util "github.com/zitadel/zitadel/internal/api/http"
@ -301,8 +301,13 @@ func startAPIs(
if accessSvc.Enabled() {
logging.Warn("access logs are currently in beta")
}
accessInterceptor := middleware.NewAccessInterceptor(accessSvc, config.Quotas.Access)
apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, accessSvc)
exhaustedCookieHandler := http_util.NewCookieHandler(
http_util.WithUnsecure(),
http_util.WithNonHttpOnly(),
http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))),
)
limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, config.Quotas.Access)
apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, limitingAccessInterceptor)
if err != nil {
return fmt.Errorf("error creating api %w", err)
}
@ -332,9 +337,12 @@ func startAPIs(
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries, permissionCheck)); err != nil {
return err
}
if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
return err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, accessInterceptor.Handle))
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources)
if err != nil {
@ -355,25 +363,25 @@ func startAPIs(
}
apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler)
oidcProvider, err := oidc.NewProvider(config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, accessInterceptor.Handle)
oidcProvider, err := oidc.NewProvider(config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor.Handle)
if err != nil {
return fmt.Errorf("unable to start oidc provider: %w", err)
}
apis.RegisterHandlerPrefixes(oidcProvider.HttpHandler(), "/.well-known/openid-configuration", "/oidc/v1", "/oauth/v2")
samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, accessInterceptor.Handle)
samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, limitingAccessInterceptor.Handle)
if err != nil {
return fmt.Errorf("unable to start saml provider: %w", err)
}
apis.RegisterHandlerOnPrefix(saml.HandlerPrefix, samlProvider.HttpHandler())
c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, accessInterceptor.Handle, config.CustomerPortal)
c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal)
if err != nil {
return fmt.Errorf("unable to start console: %w", err)
}
apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, accessInterceptor.Handle, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
if err != nil {
return fmt.Errorf("unable to start login: %w", err)
}
@ -385,20 +393,11 @@ func startAPIs(
return nil
}
func reusePort(network, address string, conn syscall.RawConn) error {
return conn.Control(func(descriptor uintptr) {
err := syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
if err != nil {
panic(err)
}
})
}
func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls.Config, shutdown <-chan os.Signal) error {
http2Server := &http2.Server{}
http1Server := &http.Server{Handler: h2c.NewHandler(router, http2Server), TLSConfig: tlsConfig}
lc := &net.ListenConfig{Control: reusePort}
lc := listenConfig()
lis, err := lc.Listen(ctx, "tcp", fmt.Sprintf(":%d", port))
if err != nil {
return fmt.Errorf("tcp listener on %d failed: %w", port, err)

11
cmd/start/start_port.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !integration
package start
import (
"net"
)
func listenConfig() *net.ListenConfig {
return &net.ListenConfig{}
}

View File

@ -0,0 +1,25 @@
//go:build integration
package start
import (
"net"
"syscall"
"golang.org/x/sys/unix"
)
func listenConfig() *net.ListenConfig {
return &net.ListenConfig{
Control: reusePort,
}
}
func reusePort(network, address string, conn syscall.RawConn) error {
return conn.Control(func(descriptor uintptr) {
err := syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
if err != nil {
panic(err)
}
})
}

View File

@ -30,19 +30,23 @@
<span class="fill-space"></span>
<div class="app-specs cnsl-secondary-text">
<div class="row" *ngIf="isOIDC && method && method.responseType !== undefined">
<span>{{ 'APP.OIDC.RESPONSETYPE' | translate }}</span>
<span class="row-entry">{{ 'APP.OIDC.RESPONSETYPE' | translate }}</span>
<span>{{ 'APP.OIDC.RESPONSE.' + method.responseType.toString() | translate }}</span>
</div>
<div class="row" *ngIf="isOIDC && method.grantType !== undefined">
<span>{{ 'APP.GRANT' | translate }}</span>
<span>{{ 'APP.OIDC.GRANT.' + method.grantType.toString() | translate }}</span>
<span class="row-entry">{{ 'APP.GRANT' | translate }}</span>
<span
><span class="space" *ngFor="let grant of method.grantType">{{
'APP.OIDC.GRANT.' + grant.toString() | translate
}}</span></span
>
</div>
<div class="row" *ngIf="isOIDC && method.authMethod !== undefined">
<span>{{ 'APP.AUTHMETHOD' | translate }}</span>
<span class="row-entry">{{ 'APP.AUTHMETHOD' | translate }}</span>
<span>{{ 'APP.OIDC.AUTHMETHOD.' + method.authMethod.toString() | translate }}</span>
</div>
<div class="row" *ngIf="!isOIDC && method.apiAuthMethod !== undefined">
<span>{{ 'APP.AUTHMETHOD' | translate }}</span>
<span class="row-entry">{{ 'APP.AUTHMETHOD' | translate }}</span>
<span>{{ 'APP.API.AUTHMETHOD.' + method.apiAuthMethod.toString() | translate }}</span>
</div>
</div>

View File

@ -155,7 +155,11 @@
white-space: nowrap;
}
:first-child {
.space {
margin-left: 0.5rem;
}
.row-entry {
margin-right: 1rem;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -14,7 +14,7 @@ export interface RadioItemAuthType {
prefix: string;
background: string;
responseType?: OIDCResponseType;
grantType?: OIDCGrantType;
grantType?: OIDCGrantType[];
authMethod?: OIDCAuthMethodType;
apiAuthMethod?: APIAuthMethodType;
recommended?: boolean;

View File

@ -58,13 +58,9 @@
</form>
</mat-step>
<!-- skip for native OIDC and SAML applications -->
<!-- skip for SAML applications -->
<mat-step
*ngIf="
(appType?.value?.createType === AppCreateType.OIDC &&
appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE) ||
appType?.value?.createType === AppCreateType.API
"
*ngIf="appType?.value?.createType === AppCreateType.OIDC || appType?.value?.createType === AppCreateType.API"
[stepControl]="secondFormGroup"
[editable]="true"
>
@ -93,9 +89,11 @@
</div>
</form>
</mat-step>
<!-- show redirect step only for OIDC apps -->
<mat-step *ngIf="appType?.value?.createType === AppCreateType.OIDC" [editable]="true">
<mat-step
*ngIf="appType?.value?.createType === AppCreateType.OIDC && authMethod?.value !== 'DEVICECODE'"
[editable]="true"
>
<ng-template matStepLabel>{{ 'APP.OIDC.REDIRECTSECTION' | translate }}</ng-template>
<p class="step-title">{{ 'APP.OIDC.REDIRECTTITLE' | translate }}</p>
@ -431,7 +429,13 @@
</ng-container>
</div>
<div class="content" *ngIf="formappType?.value?.createType === AppCreateType.OIDC">
<div
class="content"
*ngIf="
formappType?.value?.createType === AppCreateType.OIDC &&
!(oidcAppRequest.toObject().appType === OIDCAppType.OIDC_APP_TYPE_NATIVE && grantTypesListContainsOnlyDeviceCode)
"
>
<div class="formfield full-width">
<cnsl-redirect-uris
class="redirect-section"

View File

@ -32,6 +32,7 @@ import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog
import {
BASIC_AUTH_METHOD,
CODE_METHOD,
DEVICE_CODE_METHOD,
getPartialConfigFromAuthMethod,
IMPLICIT_METHOD,
PKCE_METHOD,
@ -112,6 +113,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
{ type: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, checked: true, disabled: false },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, checked: false, disabled: true },
];
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
@ -163,7 +165,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
switch (this.appType?.value.oidcAppType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [PKCE_METHOD];
this.authMethods = [PKCE_METHOD, DEVICE_CODE_METHOD];
// automatically set to PKCE and skip step
this.oidcAppRequest.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE]);
@ -473,6 +475,13 @@ export class AppCreateComponent implements OnInit, OnDestroy {
return this.form.get('grantTypesList');
}
get grantTypesListContainsOnlyDeviceCode(): boolean {
return (
this.oidcAppRequest.toObject().grantTypesList.length === 1 &&
this.oidcAppRequest.toObject().grantTypesList[0] === OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE
);
}
get formappType(): AbstractControl | null {
return this.form.get('appType');
}
@ -480,9 +489,6 @@ export class AppCreateComponent implements OnInit, OnDestroy {
get formMetadataUrl(): AbstractControl | null {
return this.form.get('metadataUrl');
}
// get formapplicationType(): AbstractControl | null {
// return this.form.get('applicationType');
// }
get authMethodType(): AbstractControl | null {
return this.form.get('authMethodType');

View File

@ -46,6 +46,7 @@ import {
BASIC_AUTH_METHOD,
CODE_METHOD,
CUSTOM_METHOD,
DEVICE_CODE_METHOD,
getAuthMethodFromPartialConfig,
getPartialConfigFromAuthMethod,
IMPLICIT_METHOD,
@ -89,6 +90,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
public oidcGrantTypes: OIDCGrantType[] = [
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
];
public oidcAppTypes: OIDCAppType[] = [
@ -274,6 +276,16 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (this.app.oidcConfig) {
this.getAuthMethodOptions('OIDC');
if (
this.app.oidcConfig.grantTypesList.length === 1 &&
this.app.oidcConfig.grantTypesList[0] === OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE
) {
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
{ id: 'token', i18nKey: 'APP.TOKEN' },
{ id: 'urls', i18nKey: 'APP.URLS' },
];
} else {
this.settingsList = [
{ id: 'configuration', i18nKey: 'APP.CONFIGURATION' },
{ id: 'token', i18nKey: 'APP.TOKEN' },
@ -281,6 +293,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
{ id: 'additional-origins', i18nKey: 'APP.ADDITIONALORIGINS' },
{ id: 'urls', i18nKey: 'APP.URLS' },
];
}
this.initialAuthMethod = this.authMethodFromPartialConfig({ oidc: this.app.oidcConfig });
this.currentAuthMethod = this.initialAuthMethod;
@ -381,7 +394,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
if (type === 'OIDC') {
switch (this.app?.oidcConfig?.appType) {
case OIDCAppType.OIDC_APP_TYPE_NATIVE:
this.authMethods = [PKCE_METHOD, CUSTOM_METHOD];
this.authMethods = [PKCE_METHOD, DEVICE_CODE_METHOD, CUSTOM_METHOD];
break;
case OIDCAppType.OIDC_APP_TYPE_WEB:
this.authMethods = [PKCE_METHOD, CODE_METHOD, PK_JWT_METHOD, POST_METHOD];

View File

@ -16,10 +16,11 @@ export const CODE_METHOD: RadioItemAuthType = {
prefix: 'CODE',
background: 'linear-gradient(40deg, rgb(25 105 143) 30%, rgb(23 95 129))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
recommended: false,
};
export const PKCE_METHOD: RadioItemAuthType = {
key: 'PKCE',
titleI18nKey: 'APP.AUTHMETHODS.PKCE.TITLE',
@ -28,10 +29,11 @@ export const PKCE_METHOD: RadioItemAuthType = {
prefix: 'PKCE',
background: 'linear-gradient(40deg, #059669 30%, #047857)',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
recommended: true,
};
export const POST_METHOD: RadioItemAuthType = {
key: 'POST',
titleI18nKey: 'APP.AUTHMETHODS.POST.TITLE',
@ -40,10 +42,11 @@ export const POST_METHOD: RadioItemAuthType = {
prefix: 'POST',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
notRecommended: true,
};
export const PK_JWT_METHOD: RadioItemAuthType = {
key: 'PK_JWT',
titleI18nKey: 'APP.AUTHMETHODS.PK_JWT.TITLE',
@ -52,11 +55,12 @@ export const PK_JWT_METHOD: RadioItemAuthType = {
prefix: 'JWT',
background: 'linear-gradient(40deg, rgb(70 77 145) 30%, rgb(58 65 124))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
// recommended: true,
};
export const BASIC_AUTH_METHOD: RadioItemAuthType = {
key: 'BASIC',
titleI18nKey: 'APP.AUTHMETHODS.BASIC.TITLE',
@ -65,7 +69,7 @@ export const BASIC_AUTH_METHOD: RadioItemAuthType = {
prefix: 'BASIC',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
apiAuthMethod: APIAuthMethodType.API_AUTH_METHOD_TYPE_BASIC,
};
@ -78,11 +82,24 @@ export const IMPLICIT_METHOD: RadioItemAuthType = {
prefix: 'IMP',
background: 'linear-gradient(40deg, #c53b3b 30%, rgb(169 51 51))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_ID_TOKEN,
grantType: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
notRecommended: true,
};
export const DEVICE_CODE_METHOD: RadioItemAuthType = {
key: 'DEVICECODE',
titleI18nKey: 'APP.AUTHMETHODS.DEVICECODE.TITLE',
descI18nKey: 'APP.AUTHMETHODS.DEVICECODE.DESCRIPTION',
disabled: false,
prefix: 'DEVICECODE',
background: 'linear-gradient(40deg, rgb(56 189 248) 30%, rgb(14 165 233))',
responseType: OIDCResponseType.OIDC_RESPONSE_TYPE_CODE,
grantType: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE],
authMethod: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
recommended: false,
};
export const CUSTOM_METHOD: RadioItemAuthType = {
key: 'CUSTOM',
titleI18nKey: 'APP.AUTHMETHODS.CUSTOM.TITLE',
@ -112,6 +129,15 @@ export function getPartialConfigFromAuthMethod(authMethod: string):
},
};
return config;
case DEVICE_CODE_METHOD.key:
config = {
oidc: {
responseTypesList: [OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
grantTypesList: [OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE],
authMethodType: OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
},
};
return config;
case PKCE_METHOD.key:
config = {
oidc: {
@ -211,6 +237,38 @@ export function getAuthMethodFromPartialConfig(config: {
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
]);
const deviceCode = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const deviceCodeWithCode = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
// OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const deviceCodeWithCodeAndRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const deviceCodeWithRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const pkjwt = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
@ -245,6 +303,15 @@ export function getAuthMethodFromPartialConfig(config: {
case postWithRefresh:
return POST_METHOD.key;
case deviceCode:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithCode:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithRefresh:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithCodeAndRefresh:
return DEVICE_CODE_METHOD.key;
case pkjwt:
return PK_JWT_METHOD.key;
case pkjwtWithRefresh:

View File

@ -1965,7 +1965,8 @@
"GRANT": {
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token"
"2": "Refresh Token",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Basic",
@ -2056,6 +2057,10 @@
"TITLE": "Implicit",
"DESCRIPTION": "Erhalte die Token direkt vom authorize Endpoint"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autorisieren Sie das Gerät auf einem Computer oder Smartphone."
},
"CUSTOM": {
"TITLE": "Custom",
"DESCRIPTION": "Deine Konfiguration entspricht keiner anderen Option."

View File

@ -1962,7 +1962,8 @@
"GRANT": {
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token"
"2": "Refresh Token",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Basic",
@ -2053,6 +2054,10 @@
"TITLE": "Implicit",
"DESCRIPTION": "Get the tokens directly from the authorization endpoint"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Authorize the device on a computer or smartphone."
},
"CUSTOM": {
"TITLE": "Custom",
"DESCRIPTION": "Your setting doesn't correspond to any other option."

View File

@ -1962,7 +1962,8 @@
"GRANT": {
"0": "Código de autorización",
"1": "Implícito",
"2": "Token de refresco"
"2": "Token de refresco",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Básico",
@ -2053,6 +2054,10 @@
"TITLE": "Implícita",
"DESCRIPTION": "Obtén los tokens directamente del endpoint de autorización"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autorizar el dispositivo en una computadora o teléfono."
},
"CUSTOM": {
"TITLE": "Personalizada",
"DESCRIPTION": "Tu configuración no se corresponde con alguna de las otras opciones."

View File

@ -1966,7 +1966,8 @@
"GRANT": {
"0": "Code d'autorisation",
"1": "Implicite",
"2": "Rafraîchir le jeton"
"2": "Rafraîchir le jeton",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Basic",
@ -2045,6 +2046,10 @@
"TITLE": "Implicite",
"DESCRIPTION": "Obtenir les jetons directement à partir du point final d'autorisation"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autoriser l'appareil sur un ordinateur ou un smartphone."
},
"CUSTOM": {
"TITLE": "Personnalisé",
"DESCRIPTION": "Votre paramètre ne correspond à aucune autre option."

View File

@ -1967,7 +1967,8 @@
"GRANT": {
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token"
"2": "Refresh Token",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Basic",
@ -2058,6 +2059,10 @@
"TITLE": "Implicit",
"DESCRIPTION": "Ottenere i token direttamente dall'endpoint di autorizzazione"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autorizza il dispositivo su un computer o uno smartphone."
},
"CUSTOM": {
"TITLE": "Custom",
"DESCRIPTION": "La tua impostazione non corrisponde a nessun'altra opzione."

View File

@ -1957,7 +1957,8 @@
"GRANT": {
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token"
"2": "Refresh Token",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Basic",
@ -2048,6 +2049,10 @@
"TITLE": "Implicit",
"DESCRIPTION": "認証エンドポイントから直接トークンを取得します。"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "コンピューターまたはスマートフォンでデバイスを認証します。"
},
"CUSTOM": {
"TITLE": "Custom",
"DESCRIPTION": "設定は他のオプションに対応していません。"

View File

@ -1966,7 +1966,8 @@
"GRANT": {
"0": "Kod autoryzacyjny",
"1": "Implicite",
"2": "Token odświeżający"
"2": "Token odświeżający",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Podstawowy",
@ -2057,6 +2058,10 @@
"TITLE": "Implicit",
"DESCRIPTION": "Pobierz tokeny bezpośrednio z punktu autoryzacyjnego"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "Autoryzuj urządzenie na komputerze lub smartfonie."
},
"CUSTOM": {
"TITLE": "Niestandardowy",
"DESCRIPTION": "Twoje ustawienie nie odpowiada żadnej innej opcji."

View File

@ -1965,7 +1965,8 @@
"GRANT": {
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token"
"2": "Refresh Token",
"3": "Device Code"
},
"AUTHMETHOD": {
"0": "Basic",
@ -2044,6 +2045,10 @@
"TITLE": "Implicit",
"DESCRIPTION": "直接从授权端点获取令牌"
},
"DEVICECODE": {
"TITLE": "Device Code",
"DESCRIPTION": "在计算机或智能手机上授权设备。"
},
"CUSTOM": {
"TITLE": "Custom",
"DESCRIPTION": "您的设置与任何其他选项都不对应。"

12
docs/.gitignore vendored
View File

@ -9,6 +9,18 @@
.cache-loader
.artifacts
# Generated by docusaurus-plugin-openapi-docs
docs/apis/auth
docs/apis/mgmt
docs/apis/admin
docs/apis/system
docs/apis/user_service
docs/apis/session_service
docs/apis/system
docs/apis/user_service
docs/apis/session_service
docs/apis/settings_service
# Misc
.DS_Store
.env.local

View File

@ -1,27 +0,0 @@
---
title: Overview
---
import {ListElement, ListWrapper, ICONTYPE} from '../../src/components/list';
import Column from '../../src/components/column';
This section contains important agreements, policies and appendices relevant for users of our websites and services.
All documents will be provided in English language.
<Column>
<ListWrapper title="Main documents">
<ListElement link="/docs/legal/terms-of-service" type={ICONTYPE.POLICY} title="Terms of Service" description="" />
<ListElement link="/docs/legal/data-processing-agreement" type={ICONTYPE.POLICY} title="Data Processing Agreement" description="" />
<ListElement link="/docs/legal/privacy-policy" type={ICONTYPE.POLICY} title="Privacy Policy" description="" />
</ListWrapper>
<ListWrapper title="Service">
<ListElement link="/docs/legal/service-level-description" type={ICONTYPE.SERVICE} title="Service Level" description="Service levels offered by ZITADEL Cloud" />
<ListElement link="/docs/legal/cloud-service-description" type={ICONTYPE.SERVICE} title="Cloud Service" description="Service description and data location" />
<ListElement link="/docs/legal/terms-of-service-dedicated" type={ICONTYPE.SERVICE} title="Dedicated Instances" description="Terms and Conditions of dedicated Instances" />
</ListWrapper>
<ListWrapper title="Annexes">
<ListElement link="/docs/legal/support-services" type={ICONTYPE.POLICY} title="Support Services" description="Support services offered by ZITADEL and CAOS Ltd." />
<ListElement link="/docs/legal/acceptable-use-policy" type={ICONTYPE.POLICY} title="Acceptable Use Policy" description="Obligations while using ZITADEL Services" />
<ListElement link="/docs/legal/rate-limit-policy" type={ICONTYPE.POLICY} title="Rate Limit Policy" description="How ZITADEL will use rate limiting" />
</ListWrapper>
</Column>

View File

@ -1,39 +0,0 @@
---
title: Dedicated Instance Terms
custom_edit_url: null
---
## General
Last revised: June 3, 2022
### Background
Within the scope of the Framework Agreement, the Customer may choose to purchase a subscription that requires a dedicated instance of ZITADEL. These additional terms for dedicated instance ("**Dedicated Instance Terms**") apply in addition to the Framework Agreement.
### Service
CAOS operates and manages a **Dedicated Instance** of ZITADEL in a private infrastructure environment dedicated for the Customer and provides support services for the Customer according the Purchase Order, these terms, agreed [**Service Level Description**](service-level-description), and [**Support Service Descriptions**](support-services).
Each Dedicated Instance consists, except agreed otherwise in writing, of a multi-zonal high-availability configuration that guarantees loads up to the specified [rate limits](rate-limit-policy#what-rate-limits-do-apply).
### Operations
CAOS will install and manage the Dedicated Instance on infracstructure provided by preferred cloud providers. Costs for infrastructure or cloud providers are not included in the Subscription, if not agreed otherwise in writing.
You may choose to provide the required infrastructure yourself. You must comply with the requirements and prerequisites outlined in the purchase order.
You may not modify, maintain or attempt to modify the Dedicated Instance, except with prior instructions by CAOS.
CAOS will use the same backup strategy as for ZITADEL Cloud (public cloud) services, except otherwise agreed between you and CAOS in writing.
### Maintenance and Updates
We will access, modify, and maintain the Dedicated Instance at times solely determined by CAOS (**"Regular Maintenance"**).
Under certain subscription plans, the Customer may agree a custom frequency and times for changes and updates. CAOS will coordinate the cadence and the changes with the Customer. To guarantee the quality of service, maintenance will occur on regular basis, typically monthly or sooner for security or performance related patches (**"Emergency Maintenance"**), but no longer than on quarterly basis.
If you fail to permit CAOS to conduct Regular Maintenance for 3 consecutive months or Emergency Maintenance within 5 days of notification, then CAOS will raise this issue with the Customer via Escalation Process. In case the issue is not resolved 5 days after such an escalation, CAOS may terminate the subscription with 30 days prior written notice to Customer. CAOS is not obligated to provide the service according to the terms and SLA, nor is CAOS liable to any security breach or damages after failure to permit Regular Maintenance for 3 consecutive months, or Emergency Maintenance for 5 days after notification.
### Incidents
Incidents are handled as documented in the [**Support Service Descriptions**](support-services). If the Customer choose in Purchase Order to provide the required infrastructure, then any incidents related to the infrastructure of the Dedicated Instance have to be resolved through the Customer directly.

View File

@ -25,10 +25,6 @@ The following policies complement the TOS. When accepting the TOS, you accept th
* [**Acceptable Use Policy**](acceptable-use-policy) - What we understand as acceptable and fair use of our Services
* [**Rate Limit Policy**](rate-limit-policy) - How we avoid overloads of our services
This Agreement is extended with additional terms, in case your Subscription requires a Dedicated Instance. When you enter the Agreement with us, you accept these additional agreements.
* [**Dedicated Instance Terms**](terms-of-service-dedicated) - How we provide our services for a dedicated instance
### Alterations
Any provisions which deviate from these TOS must be agreed in writing between the Customer and us. Such agreements shall take precedence over the TOS outlined in this document.
@ -195,7 +191,7 @@ Should any provision of these TOS be or become invalid, this shall not affect th
These TOS shall enter into force as of 15.07.2022.
Last revised: June 14, 2022
Last revised: May 12, 2023
### Amendments

View File

@ -4,7 +4,7 @@ services:
traefik:
networks:
- 'zitadel'
image: "traefik:v2.7"
image: "traefik:v2.10.1"
ports:
- "80:80"
- "443:443"

View File

@ -1,3 +1,8 @@
log:
level: DEBUG
accessLog: {}
entrypoints:
web:
address: ":80"

View File

@ -23,3 +23,8 @@ Database:
RootCert: "/crdb-certs/ca.crt"
Cert: "/crdb-certs/client.root.crt"
Key: "/crdb-certs/client.root.key"
LogStore:
Access:
Stdout:
Enabled: true

View File

@ -70,3 +70,8 @@ This is the IAM admin users login according to your configuration in the [exampl
- **password**: *RootPassword1!*
Read more about [the login process](/guides/integrate/login-users).
## Troubleshooting
You can connect to cockroach like this: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --host my-cockroach-db --certs-dir /cockroach/certs/`
For example, to show all login names: `docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --database zitadel --host my-cockroach-db --certs-dir /cockroach/certs/ --execute "select * from projections.login_names2"`

View File

@ -83,7 +83,7 @@ module.exports = {
},
{
type: "doc",
docId: "legal/introduction",
docId: "legal",
label: "Legal",
position: "right",
},
@ -273,6 +273,13 @@ module.exports = {
sidebarOptions: {
groupPathsBy: "tag",
},
},
settings: {
specPath: ".artifacts/openapi/zitadel/settings/v2alpha/settings_service.swagger.json",
outputDir: "docs/apis/settings_service",
sidebarOptions: {
groupPathsBy: "tag",
},
}
}
},

View File

@ -15,7 +15,7 @@ http {
}
location /docs {
root /usr/share/nginx/html;
alias /usr/share/nginx/html;
index /docs/index.html;
try_files $uri $uri/ /docs/index.html?q=$query_string;
}

View File

@ -426,6 +426,20 @@ module.exports = {
},
items: require("./docs/apis/session_service/sidebar.js"),
},
{
type: "category",
label: "Settings Lifecycle (Alpha)",
link: {
type: "generated-index",
title: "Settings Service API (Alpha)",
slug: "/apis/settings_service",
description:
"This API is intended to manage settings 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/settings_service/sidebar.js"),
},
{
type: "category",
label: "Assets",
@ -526,7 +540,18 @@ module.exports = {
support: [
],
legal: [
"legal/introduction",
{
type: "category",
label: "Legal Agreements",
collapsed: false,
link: {
type: "generated-index",
title: "Legal Agreements",
slug: "legal",
description:
"This section contains important agreements, policies and appendices relevant for users of our websites and services. All documents will be provided in English language.",
},
items: [
"legal/terms-of-service",
"legal/data-processing-agreement",
{
@ -541,11 +566,10 @@ module.exports = {
},
{
type: "category",
label: "Additional terms",
label: "Support Program",
collapsed: true,
items: [
"legal/terms-support-service",
"legal/terms-of-service-dedicated",
],
},
{
@ -559,5 +583,7 @@ module.exports = {
"legal/vulnerability-disclosure-policy",
],
},
]
},
],
};

View File

@ -2,7 +2,8 @@ import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from '../
import { Context } from 'support/commands';
const testProjectName = 'e2eprojectapplication';
const testAppName = 'e2eappundertest';
const testPKCEAppName = 'e2eapppkcetest';
const testDEVICECODEAppName = 'e2eappdevicecodetest';
describe('applications', () => {
beforeEach(() => {
@ -17,15 +18,15 @@ describe('applications', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testAppName);
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testPKCEAppName);
cy.visit(`/projects/${projectId}`);
});
});
});
it('add app', () => {
it('add web pkce app', () => {
cy.get('[data-e2e="app-card-add"]').should('be.visible').click();
cy.get('[formcontrolname="name"]').focus().type(testAppName);
cy.get('[formcontrolname="name"]').focus().type(testPKCEAppName);
cy.get('[for="WEB"]').click();
cy.get('[data-e2e="continue-button-nameandtype"]').click();
cy.get('[for="PKCE"]').should('be.visible').click();
@ -43,6 +44,33 @@ describe('applications', () => {
});
});
describe('add native device code app', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testDEVICECODEAppName);
cy.visit(`/projects/${projectId}`);
});
});
});
it('add device code app', () => {
cy.get('[data-e2e="app-card-add"]').should('be.visible').click();
cy.get('[formcontrolname="name"]').focus().type(testDEVICECODEAppName);
cy.get('[for="N"]').click();
cy.get('[data-e2e="continue-button-nameandtype"]').click();
cy.get('[for="DEVICECODE"]').should('be.visible').click();
cy.get('[data-e2e="continue-button-authmethod"]').click();
cy.get('[data-e2e="create-button"]').click();
cy.get('[id*=overlay]').should('exist');
cy.shouldConfirmSuccess();
const expectClientId = new RegExp(`^.*[0-9]+\\@${testProjectName}.*$`);
cy.get('[data-e2e="client-id-copy"]').click();
cy.contains('[data-e2e="client-id"]', expectClientId);
cy.clipboardMatches(expectClientId);
});
});
describe('edit app', () => {
it('should configure an application to enable dev mode');
it('should configure an application to put user roles and info inside id token');

View File

@ -107,14 +107,9 @@ describe('quotas', () => {
},
});
});
expectCookieDoesntExist();
const expiresMax = new Date();
expiresMax.setMinutes(expiresMax.getMinutes() + 2);
cy.getCookie('zitadel.quota.limiting').then((cookie) => {
expect(cookie.value).to.equal('false');
const cookieExpiry = new Date();
cookieExpiry.setTime(cookie.expiry * 1000);
expect(cookieExpiry).to.be.within(start, expiresMax);
});
cy.request({
url: urls[0],
method: 'GET',
@ -127,12 +122,16 @@ describe('quotas', () => {
});
cy.getCookie('zitadel.quota.limiting').then((cookie) => {
expect(cookie.value).to.equal('true');
const cookieExpiry = new Date();
cookieExpiry.setTime(cookie.expiry * 1000);
expect(cookieExpiry).to.be.within(start, expiresMax);
});
createHumanUser(ctx.api, testUserName, false).then((res) => {
expect(res.status).to.equal(429);
});
ensureQuotaIsRemoved(ctx, Unit.AuthenticatedRequests);
createHumanUser(ctx.api, testUserName);
expectCookieDoesntExist();
});
});
});
@ -301,3 +300,9 @@ describe('quotas', () => {
});
});
});
function expectCookieDoesntExist() {
cy.getCookie('zitadel.quota.limiting').then((cookie) => {
expect(cookie).to.be.null;
});
}

View File

@ -20,7 +20,6 @@ import (
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/metrics"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
@ -35,6 +34,8 @@ type API struct {
http1HostName string
grpcGateway *server.Gateway
healthServer *health.Server
accessInterceptor *http_mw.AccessInterceptor
queries *query.Queries
}
type healthCheck interface {
@ -49,7 +50,7 @@ func New(
verifier *internal_authz.TokenVerifier,
authZ internal_authz.Config,
tlsConfig *tls.Config, http2HostName, http1HostName string,
accessSvc *logstore.Service,
accessInterceptor *http_mw.AccessInterceptor,
) (_ *API, err error) {
api := &API{
port: port,
@ -57,10 +58,12 @@ func New(
health: queries,
router: router,
http1HostName: http1HostName,
queries: queries,
accessInterceptor: accessInterceptor,
}
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, tlsConfig, accessSvc)
api.grpcGateway, err = server.CreateGateway(ctx, port, http1HostName)
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, tlsConfig, accessInterceptor.AccessService())
api.grpcGateway, err = server.CreateGateway(ctx, port, http1HostName, accessInterceptor)
if err != nil {
return nil, err
}
@ -79,7 +82,14 @@ func New(
// used for v1 api (system, admin, mgmt, auth)
func (a *API) RegisterServer(ctx context.Context, grpcServer server.WithGatewayPrefix) error {
grpcServer.RegisterServer(a.grpcServer)
handler, prefix, err := server.CreateGatewayWithPrefix(ctx, grpcServer, a.port, a.http1HostName)
handler, prefix, err := server.CreateGatewayWithPrefix(
ctx,
grpcServer,
a.port,
a.http1HostName,
a.accessInterceptor,
a.queries,
)
if err != nil {
return err
}

View File

@ -1,8 +1,11 @@
package object
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
@ -36,3 +39,13 @@ func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool)
}
return query.Offset, uint64(query.Limit), query.Asc
}
func ResourceOwnerFromReq(ctx context.Context, req *object.RequestContext) string {
if req.GetInstance() {
return authz.GetInstance(ctx).InstanceID()
}
if req.GetOrgId() != "" {
return req.GetOrgId()
}
return authz.GetCtxData(ctx).OrgID
}

View File

@ -17,6 +17,7 @@ import (
client_middleware "github.com/zitadel/zitadel/internal/api/grpc/client/middleware"
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/query"
)
const (
@ -67,10 +68,12 @@ type Gateway struct {
mux *runtime.ServeMux
http1HostName string
connection *grpc.ClientConn
accessInterceptor *http_mw.AccessInterceptor
queries *query.Queries
}
func (g *Gateway) Handler() http.Handler {
return addInterceptors(g.mux, g.http1HostName)
return addInterceptors(g.mux, g.http1HostName, g.accessInterceptor, g.queries)
}
type CustomHTTPResponse interface {
@ -79,7 +82,14 @@ type CustomHTTPResponse interface {
type RegisterGatewayFunc func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error
func CreateGatewayWithPrefix(ctx context.Context, g WithGatewayPrefix, port uint16, http1HostName string) (http.Handler, string, error) {
func CreateGatewayWithPrefix(
ctx context.Context,
g WithGatewayPrefix,
port uint16,
http1HostName string,
accessInterceptor *http_mw.AccessInterceptor,
queries *query.Queries,
) (http.Handler, string, error) {
runtimeMux := runtime.NewServeMux(serveMuxOptions...)
opts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
@ -93,10 +103,10 @@ func CreateGatewayWithPrefix(ctx context.Context, g WithGatewayPrefix, port uint
if err != nil {
return nil, "", fmt.Errorf("failed to register grpc gateway: %w", err)
}
return addInterceptors(runtimeMux, http1HostName), g.GatewayPathPrefix(), nil
return addInterceptors(runtimeMux, http1HostName, accessInterceptor, queries), g.GatewayPathPrefix(), nil
}
func CreateGateway(ctx context.Context, port uint16, http1HostName string) (*Gateway, error) {
func CreateGateway(ctx context.Context, port uint16, http1HostName string, accessInterceptor *http_mw.AccessInterceptor) (*Gateway, error) {
connection, err := dial(ctx,
port,
[]grpc.DialOption{
@ -111,6 +121,7 @@ func CreateGateway(ctx context.Context, port uint16, http1HostName string) (*Gat
mux: runtimeMux,
http1HostName: http1HostName,
connection: connection,
accessInterceptor: accessInterceptor,
}, nil
}
@ -145,13 +156,22 @@ func dial(ctx context.Context, port uint16, opts []grpc.DialOption) (*grpc.Clien
return conn, nil
}
func addInterceptors(handler http.Handler, http1HostName string) http.Handler {
func addInterceptors(
handler http.Handler,
http1HostName string,
accessInterceptor *http_mw.AccessInterceptor,
queries *query.Queries,
) http.Handler {
handler = http_mw.CallDurationHandler(handler)
handler = http1Host(handler, http1HostName)
handler = http_mw.CORSInterceptor(handler)
handler = http_mw.RobotsTagHandler(handler)
handler = http_mw.DefaultTelemetryHandler(handler)
return http_mw.DefaultMetricsHandler(handler)
// For some non-obvious reason, the exhaustedCookieInterceptor sends the SetCookie header
// only if it follows the http_mw.DefaultTelemetryHandler
handler = exhaustedCookieInterceptor(handler, accessInterceptor, queries)
handler = http_mw.DefaultMetricsHandler(handler)
return handler
}
func http1Host(next http.Handler, http1HostName string) http.Handler {
@ -165,3 +185,35 @@ func http1Host(next http.Handler, http1HostName string) http.Handler {
next.ServeHTTP(w, r)
})
}
func exhaustedCookieInterceptor(
next http.Handler,
accessInterceptor *http_mw.AccessInterceptor,
queries *query.Queries,
) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
next.ServeHTTP(&cookieResponseWriter{
ResponseWriter: writer,
accessInterceptor: accessInterceptor,
request: request,
queries: queries,
}, request)
})
}
type cookieResponseWriter struct {
http.ResponseWriter
accessInterceptor *http_mw.AccessInterceptor
request *http.Request
queries *query.Queries
}
func (r *cookieResponseWriter) WriteHeader(status int) {
if status >= 200 && status < 300 {
r.accessInterceptor.DeleteExhaustedCookie(r.ResponseWriter, r.request)
}
if status == http.StatusTooManyRequests {
r.accessInterceptor.SetExhaustedCookie(r.ResponseWriter, r.request)
}
r.ResponseWriter.WriteHeader(status)
}

View File

@ -0,0 +1,57 @@
package settings
import (
"context"
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/assets"
"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/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
)
var _ settings.SettingsServiceServer = (*Server)(nil)
type Server struct {
settings.UnimplementedSettingsServiceServer
command *command.Commands
query *query.Queries
assetsAPIDomain func(context.Context) string
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
externalSecure bool,
) *Server {
return &Server{
command: command,
query: query,
assetsAPIDomain: assets.AssetAPI(externalSecure),
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
settings.RegisterSettingsServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return settings.SettingsService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return settings.SettingsService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return settings.SettingsService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return settings.RegisterSettingsServiceHandler
}

View File

@ -0,0 +1,129 @@
package settings
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/api/grpc/text"
"github.com/zitadel/zitadel/internal/query"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
)
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {
current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetLoginSettingsResponse{
Settings: loginSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.OrgID,
},
}, nil
}
func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) {
current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetPasswordComplexitySettingsResponse{
Settings: passwordSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) {
current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetBrandingSettingsResponse{
Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) {
current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetDomainSettingsResponse{
Settings: domainSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) {
current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetLegalAndSupportSettingsResponse{
Settings: legalAndSupportSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) {
current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetLockoutSettingsResponse{
Settings: lockoutSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) {
links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{}, false)
if err != nil {
return nil, err
}
return &settings.GetActiveIdentityProvidersResponse{
Details: object.ToListDetails(links.SearchResponse),
IdentityProviders: identityProvidersToPb(links.Links),
}, nil
}
func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) {
langs, err := s.query.Languages(ctx)
if err != nil {
return nil, err
}
instance := authz.GetInstance(ctx)
return &settings.GetGeneralSettingsResponse{
SupportedLanguages: text.LanguageTagsToStrings(langs),
DefaultOrgId: instance.DefaultOrganisationID(),
DefaultLanguage: instance.DefaultLanguage().String(),
}, nil
}

View File

@ -0,0 +1,189 @@
package settings
import (
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
)
// TODO: ?
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
multi := make([]settings.MultiFactorType, len(current.MultiFactors))
for i, typ := range current.MultiFactors {
multi[i] = multiFactorTypeToPb(typ)
}
second := make([]settings.SecondFactorType, len(current.SecondFactors))
for i, typ := range current.SecondFactors {
second[i] = secondFactorTypeToPb(typ)
}
return &settings.LoginSettings{
AllowUsernamePassword: current.AllowUsernamePassword,
AllowRegister: current.AllowRegister,
AllowExternalIdp: current.AllowExternalIDPs,
ForceMfa: current.ForceMFA,
PasskeysType: passkeysTypeToPb(current.PasswordlessType),
HidePasswordReset: current.HidePasswordReset,
IgnoreUnknownUsernames: current.IgnoreUnknownUsernames,
AllowDomainDiscovery: current.AllowDomainDiscovery,
DisableLoginWithEmail: current.DisableLoginWithEmail,
DisableLoginWithPhone: current.DisableLoginWithPhone,
DefaultRedirectUri: current.DefaultRedirectURI,
PasswordCheckLifetime: durationpb.New(current.PasswordCheckLifetime),
ExternalLoginCheckLifetime: durationpb.New(current.ExternalLoginCheckLifetime),
MfaInitSkipLifetime: durationpb.New(current.MFAInitSkipLifetime),
SecondFactorCheckLifetime: durationpb.New(current.SecondFactorCheckLifetime),
MultiFactorCheckLifetime: durationpb.New(current.MultiFactorCheckLifetime),
SecondFactors: second,
MultiFactors: multi,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func isDefaultToResourceOwnerTypePb(isDefault bool) settings.ResourceOwnerType {
if isDefault {
return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE
}
return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG
}
func passkeysTypeToPb(passwordlessType domain.PasswordlessType) settings.PasskeysType {
switch passwordlessType {
case domain.PasswordlessTypeAllowed:
return settings.PasskeysType_PASSKEYS_TYPE_ALLOWED
case domain.PasswordlessTypeNotAllowed:
return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED
default:
return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED
}
}
func secondFactorTypeToPb(secondFactorType domain.SecondFactorType) settings.SecondFactorType {
switch secondFactorType {
case domain.SecondFactorTypeOTP:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP
case domain.SecondFactorTypeU2F:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F
case domain.SecondFactorTypeUnspecified:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
default:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
}
}
func multiFactorTypeToPb(typ domain.MultiFactorType) settings.MultiFactorType {
switch typ {
case domain.MultiFactorTypeU2FWithPIN:
return settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION
case domain.MultiFactorTypeUnspecified:
return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED
default:
return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED
}
}
func passwordSettingsToPb(current *query.PasswordComplexityPolicy) *settings.PasswordComplexitySettings {
return &settings.PasswordComplexitySettings{
MinLength: current.MinLength,
RequiresUppercase: current.HasUppercase,
RequiresLowercase: current.HasLowercase,
RequiresNumber: current.HasNumber,
RequiresSymbol: current.HasSymbol,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func brandingSettingsToPb(current *query.LabelPolicy, assetPrefix string) *settings.BrandingSettings {
return &settings.BrandingSettings{
LightTheme: themeToPb(current.Light, assetPrefix, current.ResourceOwner),
DarkTheme: themeToPb(current.Dark, assetPrefix, current.ResourceOwner),
FontUrl: domain.AssetURL(assetPrefix, current.ResourceOwner, current.FontURL),
DisableWatermark: current.WatermarkDisabled,
HideLoginNameSuffix: current.HideLoginNameSuffix,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func themeToPb(theme query.Theme, assetPrefix, resourceOwner string) *settings.Theme {
return &settings.Theme{
PrimaryColor: theme.PrimaryColor,
BackgroundColor: theme.BackgroundColor,
FontColor: theme.FontColor,
WarnColor: theme.WarnColor,
LogoUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.LogoURL),
IconUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.IconURL),
}
}
func domainSettingsToPb(current *query.DomainPolicy) *settings.DomainSettings {
return &settings.DomainSettings{
LoginNameIncludesDomain: current.UserLoginMustBeDomain,
RequireOrgDomainVerification: current.ValidateOrgDomains,
SmtpSenderAddressMatchesInstanceDomain: current.SMTPSenderAddressMatchesInstanceDomain,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func legalAndSupportSettingsToPb(current *query.PrivacyPolicy) *settings.LegalAndSupportSettings {
return &settings.LegalAndSupportSettings{
TosLink: current.TOSLink,
PrivacyPolicyLink: current.PrivacyLink,
HelpLink: current.HelpLink,
SupportEmail: string(current.SupportEmail),
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func lockoutSettingsToPb(current *query.LockoutPolicy) *settings.LockoutSettings {
return &settings.LockoutSettings{
MaxPasswordAttempts: current.MaxPasswordAttempts,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func identityProvidersToPb(idps []*query.IDPLoginPolicyLink) []*settings.IdentityProvider {
providers := make([]*settings.IdentityProvider, len(idps))
for i, idp := range idps {
providers[i] = identityProviderToPb(idp)
}
return providers
}
func identityProviderToPb(idp *query.IDPLoginPolicyLink) *settings.IdentityProvider {
return &settings.IdentityProvider{
Id: idp.IDPID,
Name: domain.IDPName(idp.IDPName, idp.IDPType),
Type: idpTypeToPb(idp.IDPType),
}
}
func idpTypeToPb(idpType domain.IDPType) settings.IdentityProviderType {
switch idpType {
case domain.IDPTypeUnspecified:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
case domain.IDPTypeOIDC:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC
case domain.IDPTypeJWT:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT
case domain.IDPTypeOAuth:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH
case domain.IDPTypeLDAP:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP
case domain.IDPTypeAzureAD:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD
case domain.IDPTypeGitHub:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB
case domain.IDPTypeGitHubEnterprise:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES
case domain.IDPTypeGitLab:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB
case domain.IDPTypeGitLabSelfHosted:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED
case domain.IDPTypeGoogle:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE
default:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
}
}

View File

@ -0,0 +1,461 @@
package settings
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
)
var ignoreMessageTypes = map[protoreflect.FullName]bool{
"google.protobuf.Duration": true,
}
// allFieldsSet recusively checks if all values in a message
// have a non-zero value.
func allFieldsSet(t testing.TB, msg protoreflect.Message) {
md := msg.Descriptor()
name := md.FullName()
if ignoreMessageTypes[name] {
return
}
fields := md.Fields()
for i := 0; i < fields.Len(); i++ {
fd := fields.Get(i)
if !msg.Has(fd) {
t.Errorf("not all fields set in %q, missing %q", name, fd.Name())
continue
}
if fd.Kind() == protoreflect.MessageKind {
allFieldsSet(t, msg.Get(fd).Message())
}
}
}
func Test_loginSettingsToPb(t *testing.T) {
arg := &query.LoginPolicy{
AllowUsernamePassword: true,
AllowRegister: true,
AllowExternalIDPs: true,
ForceMFA: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
DisableLoginWithEmail: true,
DisableLoginWithPhone: true,
DefaultRedirectURI: "example.com",
PasswordCheckLifetime: time.Hour,
ExternalLoginCheckLifetime: time.Minute,
MFAInitSkipLifetime: time.Millisecond,
SecondFactorCheckLifetime: time.Microsecond,
MultiFactorCheckLifetime: time.Nanosecond,
SecondFactors: []domain.SecondFactorType{
domain.SecondFactorTypeOTP,
domain.SecondFactorTypeU2F,
},
MultiFactors: []domain.MultiFactorType{
domain.MultiFactorTypeU2FWithPIN,
},
IsDefault: true,
}
want := &settings.LoginSettings{
AllowUsernamePassword: true,
AllowRegister: true,
AllowExternalIdp: true,
ForceMfa: true,
PasskeysType: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
DisableLoginWithEmail: true,
DisableLoginWithPhone: true,
DefaultRedirectUri: "example.com",
PasswordCheckLifetime: durationpb.New(time.Hour),
ExternalLoginCheckLifetime: durationpb.New(time.Minute),
MfaInitSkipLifetime: durationpb.New(time.Millisecond),
SecondFactorCheckLifetime: durationpb.New(time.Microsecond),
MultiFactorCheckLifetime: durationpb.New(time.Nanosecond),
SecondFactors: []settings.SecondFactorType{
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
},
MultiFactors: []settings.MultiFactorType{
settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION,
},
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := loginSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
if !proto.Equal(got, want) {
t.Errorf("loginSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_isDefaultToResourceOwnerTypePb(t *testing.T) {
type args struct {
isDefault bool
}
tests := []struct {
args args
want settings.ResourceOwnerType
}{
{
args: args{false},
want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG,
},
{
args: args{true},
want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := isDefaultToResourceOwnerTypePb(tt.args.isDefault)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passkeysTypeToPb(t *testing.T) {
type args struct {
passwordlessType domain.PasswordlessType
}
tests := []struct {
args args
want settings.PasskeysType
}{
{
args: args{domain.PasswordlessTypeNotAllowed},
want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED,
},
{
args: args{domain.PasswordlessTypeAllowed},
want: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
},
{
args: args{99},
want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := passkeysTypeToPb(tt.args.passwordlessType)
assert.Equal(t, tt.want, got)
})
}
}
func Test_secondFactorTypeToPb(t *testing.T) {
type args struct {
secondFactorType domain.SecondFactorType
}
tests := []struct {
args args
want settings.SecondFactorType
}{
{
args: args{domain.SecondFactorTypeOTP},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
},
{
args: args{domain.SecondFactorTypeU2F},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
},
{
args: args{domain.SecondFactorTypeUnspecified},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED,
},
{
args: args{99},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := secondFactorTypeToPb(tt.args.secondFactorType)
assert.Equal(t, tt.want, got)
})
}
}
func Test_multiFactorTypeToPb(t *testing.T) {
type args struct {
typ domain.MultiFactorType
}
tests := []struct {
args args
want settings.MultiFactorType
}{
{
args: args{domain.MultiFactorTypeU2FWithPIN},
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION,
},
{
args: args{domain.MultiFactorTypeUnspecified},
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED,
},
{
args: args{99},
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := multiFactorTypeToPb(tt.args.typ)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passwordSettingsToPb(t *testing.T) {
arg := &query.PasswordComplexityPolicy{
MinLength: 12,
HasUppercase: true,
HasLowercase: true,
HasNumber: true,
HasSymbol: true,
IsDefault: true,
}
want := &settings.PasswordComplexitySettings{
MinLength: 12,
RequiresUppercase: true,
RequiresLowercase: true,
RequiresNumber: true,
RequiresSymbol: true,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := passwordSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
if !proto.Equal(got, want) {
t.Errorf("passwordSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_brandingSettingsToPb(t *testing.T) {
arg := &query.LabelPolicy{
Light: query.Theme{
PrimaryColor: "red",
WarnColor: "white",
BackgroundColor: "blue",
FontColor: "orange",
LogoURL: "light-logo",
IconURL: "light-icon",
},
Dark: query.Theme{
PrimaryColor: "magenta",
WarnColor: "pink",
BackgroundColor: "black",
FontColor: "white",
LogoURL: "dark-logo",
IconURL: "dark-icon",
},
ResourceOwner: "me",
FontURL: "fonts",
WatermarkDisabled: true,
HideLoginNameSuffix: true,
IsDefault: true,
}
want := &settings.BrandingSettings{
LightTheme: &settings.Theme{
PrimaryColor: "red",
WarnColor: "white",
BackgroundColor: "blue",
FontColor: "orange",
LogoUrl: "http://example.com/me/light-logo",
IconUrl: "http://example.com/me/light-icon",
},
DarkTheme: &settings.Theme{
PrimaryColor: "magenta",
WarnColor: "pink",
BackgroundColor: "black",
FontColor: "white",
LogoUrl: "http://example.com/me/dark-logo",
IconUrl: "http://example.com/me/dark-icon",
},
FontUrl: "http://example.com/me/fonts",
DisableWatermark: true,
HideLoginNameSuffix: true,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := brandingSettingsToPb(arg, "http://example.com")
allFieldsSet(t, got.ProtoReflect())
if !proto.Equal(got, want) {
t.Errorf("brandingSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_domainSettingsToPb(t *testing.T) {
arg := &query.DomainPolicy{
UserLoginMustBeDomain: true,
ValidateOrgDomains: true,
SMTPSenderAddressMatchesInstanceDomain: true,
IsDefault: true,
}
want := &settings.DomainSettings{
LoginNameIncludesDomain: true,
RequireOrgDomainVerification: true,
SmtpSenderAddressMatchesInstanceDomain: true,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := domainSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
if !proto.Equal(got, want) {
t.Errorf("domainSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_legalSettingsToPb(t *testing.T) {
arg := &query.PrivacyPolicy{
TOSLink: "http://example.com/tos",
PrivacyLink: "http://example.com/pricacy",
HelpLink: "http://example.com/help",
SupportEmail: "support@zitadel.com",
IsDefault: true,
}
want := &settings.LegalAndSupportSettings{
TosLink: "http://example.com/tos",
PrivacyPolicyLink: "http://example.com/pricacy",
HelpLink: "http://example.com/help",
SupportEmail: "support@zitadel.com",
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := legalAndSupportSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
if !proto.Equal(got, want) {
t.Errorf("legalSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_lockoutSettingsToPb(t *testing.T) {
arg := &query.LockoutPolicy{
MaxPasswordAttempts: 22,
IsDefault: true,
}
want := &settings.LockoutSettings{
MaxPasswordAttempts: 22,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := lockoutSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
if !proto.Equal(got, want) {
t.Errorf("lockoutSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_identityProvidersToPb(t *testing.T) {
arg := []*query.IDPLoginPolicyLink{
{
IDPID: "1",
IDPName: "foo",
IDPType: domain.IDPTypeOIDC,
},
{
IDPID: "2",
IDPName: "bar",
IDPType: domain.IDPTypeGitHub,
},
}
want := []*settings.IdentityProvider{
{
Id: "1",
Name: "foo",
Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC,
},
{
Id: "2",
Name: "bar",
Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB,
},
}
got := identityProvidersToPb(arg)
require.Len(t, got, len(got))
for i, v := range got {
allFieldsSet(t, v.ProtoReflect())
if !proto.Equal(v, want[i]) {
t.Errorf("identityProvidersToPb() =\n%v\nwant\n%v", got, want)
}
}
}
func Test_idpTypeToPb(t *testing.T) {
type args struct {
idpType domain.IDPType
}
tests := []struct {
args args
want settings.IdentityProviderType
}{
{
args: args{domain.IDPTypeUnspecified},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED,
},
{
args: args{domain.IDPTypeOIDC},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC,
},
{
args: args{domain.IDPTypeJWT},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT,
},
{
args: args{domain.IDPTypeOAuth},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH,
},
{
args: args{domain.IDPTypeLDAP},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP,
},
{
args: args{domain.IDPTypeAzureAD},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD,
},
{
args: args{domain.IDPTypeGitHub},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB,
},
{
args: args{domain.IDPTypeGitHubEnterprise},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES,
},
{
args: args{domain.IDPTypeGitLab},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB,
},
{
args: args{domain.IDPTypeGitLabSelfHosted},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED,
},
{
args: args{domain.IDPTypeGoogle},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE,
},
{
args: args{99},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
if got := idpTypeToPb(tt.args.idpType); !reflect.DeepEqual(got, tt.want) {
t.Errorf("idpTypeToPb() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,15 +1,17 @@
package middleware
import (
"math"
"context"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/logstore/emitters/access"
@ -20,6 +22,7 @@ type AccessInterceptor struct {
svc *logstore.Service
cookieHandler *http_utils.CookieHandler
limitConfig *AccessConfig
storeOnly bool
}
type AccessConfig struct {
@ -27,54 +30,85 @@ type AccessConfig struct {
ExhaustedCookieMaxAge time.Duration
}
func NewAccessInterceptor(svc *logstore.Service, cookieConfig *AccessConfig) *AccessInterceptor {
// NewAccessInterceptor intercepts all requests and stores them to the logstore.
// If storeOnly is false, it also checks if requests are exhausted.
// If requests are exhausted, it also returns http.StatusTooManyRequests and sets a cookie
func NewAccessInterceptor(svc *logstore.Service, cookieHandler *http_utils.CookieHandler, cookieConfig *AccessConfig) *AccessInterceptor {
return &AccessInterceptor{
svc: svc,
cookieHandler: http_utils.NewCookieHandler(
http_utils.WithUnsecure(),
http_utils.WithMaxAge(int(math.Floor(cookieConfig.ExhaustedCookieMaxAge.Seconds()))),
),
cookieHandler: cookieHandler,
limitConfig: cookieConfig,
}
}
func (a *AccessInterceptor) WithoutLimiting() *AccessInterceptor {
return &AccessInterceptor{
svc: a.svc,
cookieHandler: a.cookieHandler,
limitConfig: a.limitConfig,
storeOnly: true,
}
}
func (a *AccessInterceptor) AccessService() *logstore.Service {
return a.svc
}
func (a *AccessInterceptor) Limit(ctx context.Context) bool {
if !a.svc.Enabled() || a.storeOnly {
return false
}
instance := authz.GetInstance(ctx)
remaining := a.svc.Limit(ctx, instance.InstanceID())
return remaining != nil && *remaining <= 0
}
func (a *AccessInterceptor) SetExhaustedCookie(writer http.ResponseWriter, request *http.Request) {
cookieValue := "true"
host := request.Header.Get(middleware.HTTP1Host)
domain := host
if strings.ContainsAny(host, ":") {
var err error
domain, _, err = net.SplitHostPort(host)
if err != nil {
logging.WithError(err).WithField("host", host).Warning("failed to extract cookie domain from request host")
}
}
a.cookieHandler.SetCookie(writer, a.limitConfig.ExhaustedCookieKey, domain, cookieValue)
}
func (a *AccessInterceptor) DeleteExhaustedCookie(writer http.ResponseWriter, request *http.Request) {
a.cookieHandler.DeleteCookie(writer, request, a.limitConfig.ExhaustedCookieKey)
}
func (a *AccessInterceptor) Handle(next http.Handler) http.Handler {
if !a.svc.Enabled() {
return next
}
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()
var err error
tracingCtx, checkSpan := tracing.NewNamedSpan(ctx, "checkAccess")
wrappedWriter := &statusRecorder{ResponseWriter: writer, status: 0}
instance := authz.GetInstance(ctx)
remaining := a.svc.Limit(tracingCtx, instance.InstanceID())
limit := remaining != nil && *remaining == 0
a.cookieHandler.SetCookie(wrappedWriter, a.limitConfig.ExhaustedCookieKey, request.Host, strconv.FormatBool(limit))
if limit {
wrappedWriter.WriteHeader(http.StatusTooManyRequests)
wrappedWriter.ignoreWrites = true
}
limited := a.Limit(tracingCtx)
checkSpan.End()
if limited {
a.SetExhaustedCookie(wrappedWriter, request)
http.Error(wrappedWriter, "quota for authenticated requests is exhausted", http.StatusTooManyRequests)
}
if !limited && !a.storeOnly {
a.DeleteExhaustedCookie(wrappedWriter, request)
}
if !limited {
next.ServeHTTP(wrappedWriter, request)
}
tracingCtx, writeSpan := tracing.NewNamedSpan(tracingCtx, "writeAccess")
defer writeSpan.End()
requestURL := request.RequestURI
unescapedURL, err := url.QueryUnescape(requestURL)
if err != nil {
logging.WithError(err).WithField("url", requestURL).Warning("failed to unescape request url")
// err = nil is effective because of deferred tracing span end
err = nil
}
instance := authz.GetInstance(tracingCtx)
a.svc.Handle(tracingCtx, &access.Record{
LogDate: time.Now(),
Protocol: access.HTTP,

View File

@ -1,9 +1,11 @@
package console
import (
"bytes"
"embed"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
@ -24,6 +26,7 @@ import (
type Config struct {
ShortCache middleware.CacheConfig
LongCache middleware.CacheConfig
InstanceManagementURL string
}
type spaHandler struct {
@ -88,7 +91,7 @@ func (f *file) Stat() (_ fs.FileInfo, err error) {
return f, nil
}
func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, callDurationInterceptor, instanceHandler, accessInterceptor func(http.Handler) http.Handler, customerPortal string) (http.Handler, error) {
func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, callDurationInterceptor, instanceHandler func(http.Handler) http.Handler, limitingAccessInterceptor *middleware.AccessInterceptor, customerPortal string) (http.Handler, error) {
fSys, err := fs.Sub(static, "static")
if err != nil {
return nil, err
@ -103,14 +106,26 @@ func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, call
handler := mux.NewRouter()
handler.Use(callDurationInterceptor, instanceHandler, security, accessInterceptor)
handler.Use(callDurationInterceptor, instanceHandler, security, limitingAccessInterceptor.WithoutLimiting().Handle)
handler.Handle(envRequestPath, middleware.TelemetryHandler()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url := http_util.BuildOrigin(r.Host, externalSecure)
environmentJSON, err := createEnvironmentJSON(url, issuer(r), authz.GetInstance(r.Context()).ConsoleClientID(), customerPortal)
ctx := r.Context()
instance := authz.GetInstance(ctx)
instanceMgmtURL, err := templateInstanceManagementURL(config.InstanceManagementURL, instance)
if err != nil {
http.Error(w, fmt.Sprintf("unable to template instance management url for console: %v", err), http.StatusInternalServerError)
return
}
environmentJSON, err := createEnvironmentJSON(url, issuer(r), instance.ConsoleClientID(), customerPortal, instanceMgmtURL)
if err != nil {
http.Error(w, fmt.Sprintf("unable to marshal env for console: %v", err), http.StatusInternalServerError)
return
}
if limitingAccessInterceptor.Limit(ctx) {
limitingAccessInterceptor.SetExhaustedCookie(w, r)
} else {
limitingAccessInterceptor.DeleteExhaustedCookie(w, r)
}
_, err = w.Write(environmentJSON)
logging.OnError(err).Error("error serving environment.json")
})))
@ -118,6 +133,18 @@ func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, call
return handler, nil
}
func templateInstanceManagementURL(templateableCookieValue string, instance authz.Instance) (string, error) {
cookieValueTemplate, err := template.New("cookievalue").Parse(templateableCookieValue)
if err != nil {
return templateableCookieValue, err
}
cookieValue := new(bytes.Buffer)
if err = cookieValueTemplate.Execute(cookieValue, instance); err != nil {
return templateableCookieValue, err
}
return cookieValue.String(), nil
}
func csp() *middleware.CSP {
csp := middleware.DefaultSCP
csp.StyleSrc = csp.StyleSrc.AddInline()
@ -127,17 +154,19 @@ func csp() *middleware.CSP {
return &csp
}
func createEnvironmentJSON(api, issuer, clientID, customerPortal string) ([]byte, error) {
func createEnvironmentJSON(api, issuer, clientID, customerPortal, instanceMgmtUrl string) ([]byte, error) {
environment := struct {
API string `json:"api,omitempty"`
Issuer string `json:"issuer,omitempty"`
ClientID string `json:"clientid,omitempty"`
CustomerPortal string `json:"customer_portal,omitempty"`
InstanceManagementURL string `json:"instance_management_url,omitempty"`
}{
API: api,
Issuer: issuer,
ClientID: clientID,
CustomerPortal: customerPortal,
InstanceManagementURL: instanceMgmtUrl,
}
return json.Marshal(environment)
}

View File

@ -327,7 +327,12 @@ 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()
log := logging.WithError(err)
if authReq != nil {
log = log.WithField("auth_req_id", authReq.ID)
}
log.Error()
_, msg = l.getErrorMessage(r, err)
}
data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg)

View File

@ -1,5 +1,7 @@
package domain
import "github.com/zitadel/logging"
type IDPState int32
const (
@ -56,3 +58,36 @@ func (t IDPType) GetCSSClass() string {
return ""
}
}
func IDPName(name string, idpType IDPType) string {
if name != "" {
return name
}
return idpType.DisplayName()
}
// DisplayName returns the name or a default
// to be used when always a name must be displayed (e.g. login)
func (t IDPType) DisplayName() string {
switch t {
case IDPTypeGitHub:
return "GitHub"
case IDPTypeGitLab:
return "GitLab"
case IDPTypeGoogle:
return "Google"
case IDPTypeUnspecified,
IDPTypeOIDC,
IDPTypeJWT,
IDPTypeOAuth,
IDPTypeLDAP,
IDPTypeAzureAD,
IDPTypeGitHubEnterprise,
IDPTypeGitLabSelfHosted:
fallthrough
default:
// we should never get here, so log it
logging.Errorf("name of provider (type %d) is empty", t)
return ""
}
}

View File

@ -4,8 +4,6 @@ import (
"net/url"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
)
@ -70,30 +68,7 @@ func (p IDPProvider) IsValid() bool {
// DisplayName returns the name or a default
// to be used when always a name must be displayed (e.g. login)
func (p IDPProvider) DisplayName() string {
if p.Name != "" {
return p.Name
}
switch p.IDPType {
case IDPTypeGitHub:
return "GitHub"
case IDPTypeGitLab:
return "GitLab"
case IDPTypeGoogle:
return "Google"
case IDPTypeUnspecified,
IDPTypeOIDC,
IDPTypeJWT,
IDPTypeOAuth,
IDPTypeLDAP,
IDPTypeAzureAD,
IDPTypeGitHubEnterprise,
IDPTypeGitLabSelfHosted:
fallthrough
default:
// we should never get here, so log it
logging.Errorf("name of provider (type %d) is empty - id: %s", p.IDPType, p.IDPConfigID)
return ""
}
return IDPName(p.Name, p.IDPType)
}
type PasswordlessType int32

View File

@ -80,27 +80,33 @@ var (
name: projection.IDPLoginPolicyLinkOwnerRemovedCol,
table: idpLoginPolicyLinkTable,
}
idpLoginPolicyOwnerTable = loginPolicyTable.setAlias("login_policy_owner")
idpLoginPolicyOwnerIDCol = LoginPolicyColumnOrgID.setTable(idpLoginPolicyOwnerTable)
idpLoginPolicyOwnerInstanceIDCol = LoginPolicyColumnInstanceID.setTable(idpLoginPolicyOwnerTable)
idpLoginPolicyOwnerIsDefaultCol = LoginPolicyColumnIsDefault.setTable(idpLoginPolicyOwnerTable)
idpLoginPolicyOwnerOwnerRemovedCol = LoginPolicyColumnOwnerRemoved.setTable(idpLoginPolicyOwnerTable)
)
func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, queries *IDPLoginPolicyLinksSearchQuery, withOwnerRemoved bool) (idps *IDPLoginPolicyLinks, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareIDPLoginPolicyLinksQuery(ctx, q.client)
query, scan := prepareIDPLoginPolicyLinksQuery(ctx, q.client, resourceOwner)
eq := sq.Eq{
IDPLoginPolicyLinkResourceOwnerCol.identifier(): resourceOwner,
IDPLoginPolicyLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
if !withOwnerRemoved {
eq[IDPLoginPolicyLinkOwnerRemovedCol.identifier()] = false
eq[idpLoginPolicyOwnerOwnerRemovedCol.identifier()] = false
}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-FDbKW", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
if err != nil || rows.Err() != nil {
return nil, errors.ThrowInternal(err, "QUERY-ZkKUc", "Errors.Internal")
}
idps, err = scan(rows)
@ -111,7 +117,11 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string,
return idps, err
}
func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) {
func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase, resourceOwner string) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) {
resourceOwnerQuery, resourceOwnerArgs, err := prepareIDPLoginPolicyLinksResourceOwnerQuery(ctx, resourceOwner)
if err != nil {
return sq.SelectBuilder{}, nil
}
return sq.Select(
IDPLoginPolicyLinkIDPIDCol.identifier(),
IDPTemplateNameCol.identifier(),
@ -119,7 +129,12 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (s
IDPTemplateOwnerTypeCol.identifier(),
countColumn.identifier()).
From(idpLoginPolicyLinkTable.identifier()).
LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol) + db.Timetravel(call.Took(ctx))).
LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol)).
RightJoin("("+resourceOwnerQuery+") AS "+idpLoginPolicyOwnerTable.alias+" ON "+
idpLoginPolicyOwnerIDCol.identifier()+" = "+IDPLoginPolicyLinkResourceOwnerCol.identifier()+" AND "+
idpLoginPolicyOwnerInstanceIDCol.identifier()+" = "+IDPLoginPolicyLinkInstanceIDCol.identifier()+
" "+db.Timetravel(call.Took(ctx)),
resourceOwnerArgs...).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*IDPLoginPolicyLinks, error) {
links := make([]*IDPLoginPolicyLink, 0)
@ -164,3 +179,22 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (s
}, nil
}
}
func prepareIDPLoginPolicyLinksResourceOwnerQuery(ctx context.Context, resourceOwner string) (string, []interface{}, error) {
eqPolicy := sq.Eq{idpLoginPolicyOwnerInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
return sq.Select(
idpLoginPolicyOwnerIDCol.identifier(),
idpLoginPolicyOwnerInstanceIDCol.identifier(),
idpLoginPolicyOwnerOwnerRemovedCol.identifier(),
).
From(idpLoginPolicyOwnerTable.identifier()).
Where(
sq.And{
eqPolicy,
sq.Or{
sq.Eq{idpLoginPolicyOwnerIDCol.identifier(): resourceOwner},
sq.Eq{idpLoginPolicyOwnerIDCol.identifier(): authz.GetInstance(ctx).InstanceID()},
},
}).
Limit(1).OrderBy(idpLoginPolicyOwnerIsDefaultCol.identifier()).ToSql()
}

View File

@ -1,6 +1,7 @@
package query
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
@ -8,6 +9,8 @@ import (
"regexp"
"testing"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/domain"
)
@ -19,6 +22,9 @@ var (
` COUNT(*) OVER ()` +
` FROM projections.idp_login_policy_links5` +
` LEFT JOIN projections.idp_templates5 ON projections.idp_login_policy_links5.idp_id = projections.idp_templates5.id AND projections.idp_login_policy_links5.instance_id = projections.idp_templates5.instance_id` +
` RIGHT JOIN (SELECT login_policy_owner.aggregate_id, login_policy_owner.instance_id, login_policy_owner.owner_removed FROM projections.login_policies4 AS login_policy_owner` +
` WHERE (login_policy_owner.instance_id = $1 AND (login_policy_owner.aggregate_id = $2 OR login_policy_owner.aggregate_id = $3)) ORDER BY login_policy_owner.is_default LIMIT 1) AS login_policy_owner` +
` ON login_policy_owner.aggregate_id = projections.idp_login_policy_links5.resource_owner AND login_policy_owner.instance_id = projections.idp_login_policy_links5.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
loginPolicyIDPLinksCols = []string{
"idp_id",
@ -42,7 +48,9 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) {
}{
{
name: "prepareIDPsQuery found",
prepare: prepareIDPLoginPolicyLinksQuery,
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) {
return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner")
},
want: want{
sqlExpectations: mockQueries(
loginPolicyIDPLinksQuery,
@ -73,7 +81,9 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) {
},
{
name: "prepareIDPsQuery no idp",
prepare: prepareIDPLoginPolicyLinksQuery,
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) {
return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner")
},
want: want{
sqlExpectations: mockQueries(
loginPolicyIDPLinksQuery,
@ -103,7 +113,9 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) {
},
{
name: "prepareIDPsQuery sql err",
prepare: prepareIDPLoginPolicyLinksQuery,
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) {
return prepareIDPLoginPolicyLinksQuery(ctx, db, "resourceOwner")
},
want: want{
sqlExpectations: mockQueryErr(
loginPolicyIDPLinksQuery,

View File

@ -15,6 +15,13 @@ message Organisation {
}
}
message RequestContext {
oneof resource_owner {
string org_id = 1;
bool instance = 2 [(validate.rules).bool = {const: true}];
}
}
message ListQuery {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
json_schema: {

View File

@ -0,0 +1,81 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/settings/v2alpha/settings.proto";
message BrandingSettings {
Theme light_theme = 1;
Theme dark_theme = 2;
string font_url = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "url to the font used";
example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/font-180950243237405441\"";
}
];
// hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set
bool hide_login_name_suffix = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "hides the org suffix on the login form if the scope \"urn:zitadel:iam:org:domain:primary:{domainname}\" is set";
}
];
bool disable_watermark = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "boolean to disable the watermark";
}
];
// resource_owner_type returns if the setting is managed on the organization or on the instance
ResourceOwnerType resource_owner_type = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "resource_owner_type returns if the setting is managed on the organization or on the instance";
}
];
}
message Theme {
// hex value for primary color
string primary_color = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "hex value for primary color";
example: "\"#5469d4\"";
}
];
// hex value for background color
string background_color = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "hex value for background color";
example: "\"#FAFAFA\"";
}
];
// hex value for warning color
string warn_color = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "hex value for warn color";
example: "\"#CD3D56\"";
}
];
// hex value for font color
string font_color = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "hex value for font color";
example: "\"#000000\"";
}
];
// url where the logo is served
string logo_url = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "url to the logo";
example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/logo-180950416321494657\"";
}
];
// url where the icon is served
string icon_url = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "url to the icon";
example: "\"https://acme.com/assets/v1/165617850692654601/policy/label/icon-180950498874178817\"";
}
];
}

View File

@ -0,0 +1,33 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/settings/v2alpha/settings.proto";
message DomainSettings {
bool login_name_includes_domain = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the username has to end with the domain of its organization"
}
];
bool require_org_domain_verification = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if organization domains should be verified upon creation, otherwise will be created already verified"
}
];
bool smtp_sender_address_matches_instance_domain = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the SMTP sender address domain should match an existing domain on the instance"
}
];
// resource_owner_type returns if the setting is managed on the organization or on the instance
ResourceOwnerType resource_owner_type = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "resource_owner_type returns if the setting is managed on the organization or on the instance";
}
];
}

View File

@ -0,0 +1,40 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/settings/v2alpha/settings.proto";
import "validate/validate.proto";
message LegalAndSupportSettings {
string tos_link = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://zitadel.com/docs/legal/terms-of-service\"";
}
];
string privacy_policy_link = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://zitadel.com/docs/legal/privacy-policy\"";
}
];
string help_link = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://zitadel.com/docs/manuals/introduction\"";
}
];
string support_email = 4 [
(validate.rules).string = {ignore_empty: true, max_len: 320, email: true},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"support-email@test.com\"";
description: "help / support email address."
}
];
// resource_owner_type returns if the setting is managed on the organization or on the instance
ResourceOwnerType resource_owner_type = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "resource_owner_type returns if the setting is managed on the organization or on the instance";
}
];
}

View File

@ -0,0 +1,23 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/settings/v2alpha/settings.proto";
message LockoutSettings {
uint64 max_password_attempts = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum password check attempts before the account gets locked. Attempts are reset as soon as the password is entered correctly or the password is reset. If set to 0 the account will never be locked."
example: "\"10\""
}
];
// resource_owner_type returns if the settings is managed on the organization or on the instance
ResourceOwnerType resource_owner_type = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "resource_owner_type returns if the settings is managed on the organization or on the instance";
}
];
}

View File

@ -0,0 +1,143 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/settings/v2alpha/settings.proto";
import "google/protobuf/duration.proto";
message LoginSettings {
bool allow_username_password = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if a user is allowed to log in with his username and password";
}
];
bool allow_register = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if a person is allowed to register a user on this organization";
}
];
bool allow_external_idp = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if a user is allowed to add a defined identity provider. E.g. Google auth";
}
];
bool force_mfa = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if a user MUST use a multi-factor to log in";
}
];
PasskeysType passkeys_type = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if passkeys are allowed for users"
}
];
bool hide_password_reset = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if password reset link should be shown in the login screen"
}
];
bool ignore_unknown_usernames = 7 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if unknown username on login screen directly returns an error or always displays the password screen"
}
];
string default_redirect_uri = 8 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines where the user will be redirected to if the login is started without app context (e.g. from mail)";
example: "\"https://acme.com/ui/console\"";
}
];
google.protobuf.Duration password_check_lifetime = 9 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Defines after how much time the user has to re-authenticate with the password.";
example: "\"864000s\"";
}
];
google.protobuf.Duration external_login_check_lifetime = 10 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Defines after how much time the user has to re-authenticate with an external provider.";
example: "\"864000s\"";
}
];
google.protobuf.Duration mfa_init_skip_lifetime = 11 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Defines after how much time the mfa prompt will be shown again.";
example: "\"2592000s\"";
}
];
google.protobuf.Duration second_factor_check_lifetime = 12 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Defines after how long the second-factor check is valid.";
example: "\"64800s\"";
}
];
google.protobuf.Duration multi_factor_check_lifetime = 13 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Defines how long the multi-factor check is valid.";
example: "\"43200s\"";
}
];
repeated SecondFactorType second_factors = 14;
repeated MultiFactorType multi_factors = 15;
// If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organization on success.
bool allow_domain_discovery = 16 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "If set to true, the suffix (@domain.com) of an unknown username input on the login screen will be matched against the org domains and will redirect to the registration of that organization on success."
}
];
bool disable_login_with_email = 17 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the user can additionally (to the login name) be identified by their verified email address"
}
];
bool disable_login_with_phone = 18 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the user can additionally (to the login name) be identified by their verified phone number"
}
];
// resource_owner_type returns if the settings is managed on the organization or on the instance
ResourceOwnerType resource_owner_type = 19 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "resource_owner_type returns if the settings is managed on the organization or on the instance";
}
];
}
enum SecondFactorType {
SECOND_FACTOR_TYPE_UNSPECIFIED = 0;
SECOND_FACTOR_TYPE_OTP = 1;
SECOND_FACTOR_TYPE_U2F = 2;
}
enum MultiFactorType {
MULTI_FACTOR_TYPE_UNSPECIFIED = 0;
MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION = 1;
}
enum PasskeysType {
PASSKEYS_TYPE_NOT_ALLOWED = 0;
PASSKEYS_TYPE_ALLOWED = 1;
}
message IdentityProvider {
string id = 1;
string name = 2;
IdentityProviderType type = 3;
}
enum IdentityProviderType {
IDENTITY_PROVIDER_TYPE_UNSPECIFIED = 0;
IDENTITY_PROVIDER_TYPE_OIDC = 1;
IDENTITY_PROVIDER_TYPE_JWT = 2;
IDENTITY_PROVIDER_TYPE_LDAP = 3;
IDENTITY_PROVIDER_TYPE_OAUTH = 4;
IDENTITY_PROVIDER_TYPE_AZURE_AD = 5;
IDENTITY_PROVIDER_TYPE_GITHUB = 6;
IDENTITY_PROVIDER_TYPE_GITHUB_ES = 7;
IDENTITY_PROVIDER_TYPE_GITLAB = 8;
IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9;
IDENTITY_PROVIDER_TYPE_GOOGLE = 10;
}

View File

@ -0,0 +1,43 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
import "protoc-gen-openapiv2/options/annotations.proto";
import "zitadel/settings/v2alpha/settings.proto";
message PasswordComplexitySettings {
uint64 min_length = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Defines the minimum length of a password.";
example: "\"8\""
}
];
bool requires_uppercase = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the password MUST contain an upper case letter"
}
];
bool requires_lowercase = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the password MUST contain a lowercase letter"
}
];
bool requires_number = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the password MUST contain a number"
}
];
bool requires_symbol = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the password MUST contain a symbol. E.g. \"$\""
}
];
// resource_owner_type returns if the settings is managed on the organization or on the instance
ResourceOwnerType resource_owner_type = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "resource_owner_type returns if the settings is managed on the organization or on the instance";
}
];
}

View File

@ -0,0 +1,13 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
import "protoc-gen-openapiv2/options/annotations.proto";
enum ResourceOwnerType {
RESOURCE_OWNER_TYPE_UNSPECIFIED = 0;
RESOURCE_OWNER_TYPE_INSTANCE = 1;
RESOURCE_OWNER_TYPE_ORG = 2;
}

View File

@ -0,0 +1,356 @@
syntax = "proto3";
package zitadel.settings.v2alpha;
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/object/v2alpha/object.proto";
import "zitadel/settings/v2alpha/branding_settings.proto";
import "zitadel/settings/v2alpha/domain_settings.proto";
import "zitadel/settings/v2alpha/legal_settings.proto";
import "zitadel/settings/v2alpha/lockout_settings.proto";
import "zitadel/settings/v2alpha/login_settings.proto";
import "zitadel/settings/v2alpha/password_settings.proto";
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha;settings";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Settings Service";
version: "2.0-alpha";
description: "This API is intended to manage settings in a ZITADEL instance. This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
consumes: "application/grpc";
produces: "application/json";
produces: "application/grpc";
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
host: "$ZITADEL_DOMAIN";
base_path: "/";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
responses: {
key: "403";
value: {
description: "Returned when the user does not have permission to access the resource.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
service SettingsService {
// Get basic information over the instance
rpc GetGeneralSettings (GetGeneralSettingsRequest) returns (GetGeneralSettingsResponse) {
option (google.api.http) = {
get: "/v2alpha/settings"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get basic information over the instance";
description: "Return the basic information of the instance for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the login settings
rpc GetLoginSettings (GetLoginSettingsRequest) returns (GetLoginSettingsResponse) {
option (google.api.http) = {
get: "/v2alpha/settings/login"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the login settings";
description: "Return the settings for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the current active identity providers
rpc GetActiveIdentityProviders (GetActiveIdentityProvidersRequest) returns (GetActiveIdentityProvidersResponse) {
option (google.api.http) = {
get: "/v2alpha/settings/login/idps"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the current active identity providers";
description: "Return the current active identity providers for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the password complexity settings
rpc GetPasswordComplexitySettings (GetPasswordComplexitySettingsRequest) returns (GetPasswordComplexitySettingsResponse) {
option (google.api.http) = {
get: "/v2alpha/settings/password/complexity"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the password complexity settings";
description: "Return the password complexity settings for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the current active branding settings
rpc GetBrandingSettings (GetBrandingSettingsRequest) returns (GetBrandingSettingsResponse) {
option (google.api.http) = {
get: "/v2alpha/settings/branding"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the current active branding settings";
description: "Return the current active branding settings for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the domain settings
rpc GetDomainSettings (GetDomainSettingsRequest) returns (GetDomainSettingsResponse) {
option (google.api.http) = {
get: "/v2alpha/settings/domain"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the domain settings";
description: "Return the domain settings for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the legal and support settings
rpc GetLegalAndSupportSettings (GetLegalAndSupportSettingsRequest) returns (GetLegalAndSupportSettingsResponse) {
option (google.api.http) = {
get: "/v2alpha/settings/legal_support"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the legal and support settings";
description: "Return the legal settings for the requested context"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
// Get the lockout settings
rpc GetLockoutSettings (GetLockoutSettingsRequest) returns (GetLockoutSettingsResponse) {
option (google.api.http) = {
get: "/v2alpha/settings/lockout"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "policy.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Get the lockout settings";
description: "Return the lockout settings for the requested context, which define when a user will be locked"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
}
message GetLoginSettingsRequest {
zitadel.object.v2alpha.RequestContext ctx = 1;
}
message GetLoginSettingsResponse {
zitadel.object.v2alpha.Details details = 1;
zitadel.settings.v2alpha.LoginSettings settings = 2;
}
message GetPasswordComplexitySettingsRequest {
zitadel.object.v2alpha.RequestContext ctx = 1;
}
message GetPasswordComplexitySettingsResponse {
zitadel.object.v2alpha.Details details = 1;
zitadel.settings.v2alpha.PasswordComplexitySettings settings = 2;
}
message GetBrandingSettingsRequest {
zitadel.object.v2alpha.RequestContext ctx = 1;
}
message GetBrandingSettingsResponse {
zitadel.object.v2alpha.Details details = 1;
zitadel.settings.v2alpha.BrandingSettings settings = 2;
}
message GetDomainSettingsRequest {
zitadel.object.v2alpha.RequestContext ctx = 1;
}
message GetDomainSettingsResponse {
zitadel.object.v2alpha.Details details = 1;
zitadel.settings.v2alpha.DomainSettings settings = 2;
}
message GetLegalAndSupportSettingsRequest {
zitadel.object.v2alpha.RequestContext ctx = 1;
}
message GetLegalAndSupportSettingsResponse {
zitadel.object.v2alpha.Details details = 1;
zitadel.settings.v2alpha.LegalAndSupportSettings settings = 2;
}
message GetLockoutSettingsRequest {
zitadel.object.v2alpha.RequestContext ctx = 1;
}
message GetLockoutSettingsResponse {
zitadel.object.v2alpha.Details details = 1;
zitadel.settings.v2alpha.LockoutSettings settings = 2;
}
message GetActiveIdentityProvidersRequest {
zitadel.object.v2alpha.RequestContext ctx = 1;
}
message GetActiveIdentityProvidersResponse {
zitadel.object.v2alpha.ListDetails details = 1;
repeated zitadel.settings.v2alpha.IdentityProvider identity_providers = 2;
}
message GetGeneralSettingsRequest {}
message GetGeneralSettingsResponse {
string default_org_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "default organization for the current context"
}
];
string default_language = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "default language for the current context"
example: "\"en\""
}
];
repeated string supported_languages = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"en\", \"de\", \"it\"]"
}
];
}

View File

@ -229,7 +229,7 @@ service SystemService {
};
}
// Returns the domain of an instance
// Removes the domain of an instance
rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) {
option (google.api.http) = {
delete: "/instances/{instance_id}/domains/{domain}";
@ -240,7 +240,7 @@ service SystemService {
};
}
// Returns the domain of an instance
// Sets the primary domain of an instance
rpc SetPrimaryDomain(SetPrimaryDomainRequest) returns (SetPrimaryDomainResponse) {
option (google.api.http) = {
post: "/instances/{instance_id}/domains/_set_primary";