feat: specify login UI version on instance and apps (#9071)

# Which Problems Are Solved

To be able to migrate or test the new login UI, admins might want to
(temporarily) switch individual apps.
At a later point admin might want to make sure all applications use the
new login UI.

# How the Problems Are Solved

- Added a feature flag `` on instance level to require all apps to use
the new login and provide an optional base url.
- if the flag is enabled, all (OIDC) applications will automatically use
the v2 login.
  - if disabled, applications can decide based on their configuration
- Added an option on OIDC apps to use the new login UI and an optional
base url.
- Removed the requirement to use `x-zitadel-login-client` to be
redirected to the login V2 and retrieve created authrequest and link
them to SSO sessions.
- Added a new "IAM_LOGIN_CLIENT" role to allow management of users,
sessions, grants and more without `x-zitadel-login-client`.

# Additional Changes

None

# Additional Context

closes https://github.com/zitadel/zitadel/issues/8702
This commit is contained in:
Livio Spring
2024-12-19 10:37:46 +01:00
committed by GitHub
parent b5e92a6144
commit 50d2b26a28
89 changed files with 1670 additions and 321 deletions

View File

@@ -1,6 +1,10 @@
package feature
import (
"net/url"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/feature"
@@ -8,7 +12,11 @@ import (
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command.SystemFeatures, error) {
loginV2, err := loginV2ToDomain(req.GetLoginV2())
if err != nil {
return nil, err
}
return &command.SystemFeatures{
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
@@ -20,7 +28,8 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
DisableUserTokenEvent: req.DisableUserTokenEvent,
EnableBackChannelLogout: req.EnableBackChannelLogout,
}
LoginV2: loginV2,
}, nil
}
func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse {
@@ -36,10 +45,15 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
}
}
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures {
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*command.InstanceFeatures, error) {
loginV2, err := loginV2ToDomain(req.GetLoginV2())
if err != nil {
return nil, err
}
return &command.InstanceFeatures{
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
@@ -53,7 +67,8 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
DisableUserTokenEvent: req.DisableUserTokenEvent,
EnableBackChannelLogout: req.EnableBackChannelLogout,
}
LoginV2: loginV2,
}, nil
}
func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse {
@@ -71,6 +86,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
}
}
@@ -81,6 +97,39 @@ func featureSourceToImprovedPerformanceFlagPb(fs *query.FeatureSource[[]feature.
}
}
func loginV2ToDomain(loginV2 *feature_pb.LoginV2) (_ *feature.LoginV2, err error) {
if loginV2 == nil {
return nil, nil
}
var baseURI *url.URL
if loginV2.GetBaseUri() != "" {
baseURI, err = url.Parse(loginV2.GetBaseUri())
if err != nil {
return nil, err
}
}
return &feature.LoginV2{
Required: loginV2.GetRequired(),
BaseURI: baseURI,
}, nil
}
func loginV2ToLoginV2FlagPb(f query.FeatureSource[*feature.LoginV2]) *feature_pb.LoginV2FeatureFlag {
var required bool
var baseURI *string
if f.Value != nil {
required = f.Value.Required
if f.Value.BaseURI != nil && f.Value.BaseURI.String() != "" {
baseURI = gu.Ptr(f.Value.BaseURI.String())
}
}
return &feature_pb.LoginV2FeatureFlag{
Required: required,
BaseUri: baseURI,
Source: featureLevelToSourcePb(f.Level),
}
}
func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag {
return &feature_pb.FeatureFlag{
Enabled: fs.Value,

View File

@@ -1,6 +1,7 @@
package feature
import (
"net/url"
"testing"
"time"
@@ -26,6 +27,10 @@ func Test_systemFeaturesToCommand(t *testing.T) {
OidcTokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OidcSingleV1SessionTermination: gu.Ptr(true),
LoginV2: &feature_pb.LoginV2{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
},
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -36,9 +41,14 @@ func Test_systemFeaturesToCommand(t *testing.T) {
TokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OIDCSingleV1SessionTermination: gu.Ptr(true),
LoginV2: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
}
got := systemFeaturesToCommand(arg)
got, err := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
assert.NoError(t, err)
}
func Test_systemFeaturesToPb(t *testing.T) {
@@ -84,6 +94,13 @@ func Test_systemFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: true,
},
LoginV2: query.FeatureSource[*feature.LoginV2]{
Level: feature.LevelSystem,
Value: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@@ -131,6 +148,11 @@ func Test_systemFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
LoginV2: &feature_pb.LoginV2FeatureFlag{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@@ -149,6 +171,10 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
DebugOidcParentError: gu.Ptr(true),
OidcSingleV1SessionTermination: gu.Ptr(true),
EnableBackChannelLogout: gu.Ptr(true),
LoginV2: &feature_pb.LoginV2{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
},
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -162,9 +188,14 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
DebugOIDCParentError: gu.Ptr(true),
OIDCSingleV1SessionTermination: gu.Ptr(true),
EnableBackChannelLogout: gu.Ptr(true),
LoginV2: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
}
got := instanceFeaturesToCommand(arg)
got, err := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
assert.NoError(t, err)
}
func Test_instanceFeaturesToPb(t *testing.T) {
@@ -214,6 +245,13 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelInstance,
Value: true,
},
LoginV2: query.FeatureSource[*feature.LoginV2]{
Level: feature.LevelInstance,
Value: &feature.LoginV2{
Required: true,
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@@ -269,6 +307,11 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
LoginV2: &feature_pb.LoginV2FeatureFlag{
Required: true,
BaseUri: gu.Ptr("https://login.com"),
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@@ -11,7 +11,11 @@ import (
)
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req))
features, err := systemFeaturesToCommand(req)
if err != nil {
return nil, err
}
details, err := s.command.SetSystemFeatures(ctx, features)
if err != nil {
return nil, err
}
@@ -39,7 +43,11 @@ func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFe
}
func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) {
details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req))
features, err := instanceFeaturesToCommand(req)
if err != nil {
return nil, err
}
details, err := s.command.SetInstanceFeatures(ctx, features)
if err != nil {
return nil, err
}