From c71bf85b7a9dfbb7b499a6a90dfdd1924eefffa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 12 Oct 2023 15:16:59 +0300 Subject: [PATCH 01/48] feat(api/v2): store user agent details in the session (#6711) This change adds the ability to set and get user agent data, such as fingerprint, IP, request headers and a description to the session. All fields are optional. Closes #6028 --- internal/api/grpc/session/v2/session.go | 56 ++++++- .../session/v2/session_integration_test.go | 84 +++++++--- internal/api/grpc/session/v2/session_test.go | 158 +++++++++++++++++- internal/command/auth_request_test.go | 44 ++++- internal/command/oidc_session_test.go | 22 ++- internal/command/session.go | 8 +- internal/command/session_test.go | 88 ++++++++-- internal/domain/user_agent.go | 17 ++ internal/query/prepare_test.go | 12 +- internal/query/projection/session.go | 94 +++++++---- internal/query/projection/session_test.go | 38 +++-- internal/query/session.go | 33 ++++ internal/query/sessions_test.go | 107 +++++++----- internal/repository/session/session.go | 3 + proto/zitadel/session/v2beta/session.proto | 16 ++ .../session/v2beta/session_service.proto | 1 + 16 files changed, 634 insertions(+), 147 deletions(-) create mode 100644 internal/domain/user_agent.go diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index fe8e0d8744..be6907ae08 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -2,10 +2,13 @@ package session import ( "context" + "net" + "net/http" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/muhlemmer/gu" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/command" @@ -41,7 +44,7 @@ func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequ } func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) { - checks, metadata, err := s.createSessionRequestToCommand(ctx, req) + checks, metadata, userAgent, err := s.createSessionRequestToCommand(ctx, req) if err != nil { return nil, err } @@ -50,7 +53,7 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe return nil, err } - set, err := s.command.CreateSession(ctx, cmds, metadata) + set, err := s.command.CreateSession(ctx, cmds, metadata, userAgent) if err != nil { return nil, err } @@ -113,9 +116,34 @@ func sessionToPb(s *query.Session) *session.Session { Sequence: s.Sequence, Factors: factorsToPb(s), Metadata: s.Metadata, + UserAgent: userAgentToPb(s.UserAgent), } } +func userAgentToPb(ua domain.UserAgent) *session.UserAgent { + if ua.IsEmpty() { + return nil + } + + out := &session.UserAgent{ + FingerprintId: ua.FingerprintID, + Description: ua.Description, + } + if ua.IP != nil { + out.Ip = gu.Ptr(ua.IP.String()) + } + if ua.Header == nil { + return out + } + out.Header = make(map[string]*session.UserAgent_HeaderValues, len(ua.Header)) + for k, v := range ua.Header { + out.Header[k] = &session.UserAgent_HeaderValues{ + Values: v, + } + } + return out +} + func factorsToPb(s *query.Session) *session.Factors { user := userFactorToPb(s.UserFactor) if user == nil { @@ -236,12 +264,30 @@ func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) { return query.NewSessionIDsSearchQuery(q.Ids) } -func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, error) { +func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, error) { checks, err := s.checksToCommand(ctx, req.Checks) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - return checks, req.GetMetadata(), nil + return checks, req.GetMetadata(), userAgentToCommand(req.GetUserAgent()), nil +} + +func userAgentToCommand(userAgent *session.UserAgent) *domain.UserAgent { + if userAgent == nil { + return nil + } + out := &domain.UserAgent{ + FingerprintID: userAgent.FingerprintId, + IP: net.ParseIP(userAgent.GetIp()), + Description: userAgent.Description, + } + if len(userAgent.Header) > 0 { + out.Header = make(http.Header, len(userAgent.Header)) + for k, values := range userAgent.Header { + out.Header[k] = values.GetValues() + } + } + return out } func (s *Server) setSessionRequestToCommand(ctx context.Context, req *session.SetSessionRequest) ([]command.SessionCommand, error) { diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index 1dcad7bc53..9aba59ee37 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" "github.com/zitadel/zitadel/internal/integration" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" @@ -53,7 +54,7 @@ func TestMain(m *testing.M) { }()) } -func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, factors ...wantFactor) *session.Session { +func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, factors ...wantFactor) *session.Session { t.Helper() require.NotEmpty(t, id) require.NotEmpty(t, token) @@ -70,6 +71,11 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window)) assert.Equal(t, sequence, s.GetSequence()) assert.Equal(t, metadata, s.GetMetadata()) + + if !proto.Equal(userAgent, s.GetUserAgent()) { + t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent) + } + verifyFactors(t, s.GetFactors(), window, factors) return s } @@ -131,11 +137,12 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, func TestServer_CreateSession(t *testing.T) { tests := []struct { - name string - req *session.CreateSessionRequest - want *session.CreateSessionResponse - wantErr bool - wantFactors []wantFactor + name string + req *session.CreateSessionRequest + want *session.CreateSessionResponse + wantErr bool + wantFactors []wantFactor + wantUserAgent *session.UserAgent }{ { name: "empty session", @@ -148,6 +155,33 @@ func TestServer_CreateSession(t *testing.T) { }, }, }, + { + name: "user agent", + req: &session.CreateSessionRequest{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + want: &session.CreateSessionResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + wantUserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, { name: "with user", req: &session.CreateSessionRequest{ @@ -219,7 +253,7 @@ func TestServer_CreateSession(t *testing.T) { require.NoError(t, err) integration.AssertDetails(t, tt.want, got) - verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantFactors...) + verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantFactors...) }) } } @@ -242,7 +276,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) require.NoError(t, err) @@ -258,7 +292,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactorUserVerified) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactorUserVerified) } func TestServer_CreateSession_successfulIntent(t *testing.T) { @@ -274,7 +308,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id") updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ @@ -288,7 +322,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantIntentFactor) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantIntentFactor) } func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { @@ -304,7 +338,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) idpUserID := "id" intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", idpUserID) @@ -331,7 +365,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantIntentFactor) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantIntentFactor) } func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { @@ -347,7 +381,7 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) intentID := Tester.CreateIntent(t, idpID) _, err = Client.SetSession(CTX, &session.SetSessionRequest{ @@ -399,7 +433,7 @@ func TestServer_SetSession_flow(t *testing.T) { createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) require.NoError(t, err) sessionToken := createResp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil) t.Run("check user", func(t *testing.T) { resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ @@ -415,7 +449,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor) }) t.Run("check webauthn, user verified (passkey)", func(t *testing.T) { @@ -430,7 +464,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) @@ -447,7 +481,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactorUserVerified) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactorUserVerified) }) userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken) @@ -474,7 +508,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false) @@ -491,7 +525,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor) }) } }) @@ -510,7 +544,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor) }) t.Run("check OTP SMS", func(t *testing.T) { @@ -522,7 +556,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() otp := resp.GetChallenges().GetOtpSms() @@ -539,7 +573,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor) }) t.Run("check OTP Email", func(t *testing.T) { @@ -553,7 +587,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() otp := resp.GetChallenges().GetOtpEmail() @@ -570,7 +604,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor) }) } diff --git a/internal/api/grpc/session/v2/session_test.go b/internal/api/grpc/session/v2/session_test.go index 33804caba5..ae33fab4c7 100644 --- a/internal/api/grpc/session/v2/session_test.go +++ b/internal/api/grpc/session/v2/session_test.go @@ -2,15 +2,18 @@ package session import ( "context" + "net" + "net/http" "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/api/authz" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" @@ -23,7 +26,7 @@ func Test_sessionsToPb(t *testing.T) { past := now.Add(-time.Hour) sessions := []*query.Session{ - { // no factor + { // no factor, with user agent ID: "999", CreationDate: now, ChangeDate: now, @@ -32,6 +35,12 @@ func Test_sessionsToPb(t *testing.T) { ResourceOwner: "me", Creator: "he", Metadata: map[string][]byte{"hello": []byte("world")}, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerprintID"), + Description: gu.Ptr("description"), + IP: net.IPv4(1, 2, 3, 4), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, }, { // user factor ID: "999", @@ -114,13 +123,21 @@ func Test_sessionsToPb(t *testing.T) { } want := []*session.Session{ - { // no factor + { // no factor, with user agent Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), Sequence: 123, Factors: nil, Metadata: map[string][]byte{"hello": []byte("world")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerprintID"), + Description: gu.Ptr("description"), + Ip: gu.Ptr("1.2.3.4"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, }, { // user factor Id: "999", @@ -208,6 +225,71 @@ func Test_sessionsToPb(t *testing.T) { } } +func Test_userAgentToPb(t *testing.T) { + type args struct { + ua domain.UserAgent + } + tests := []struct { + name string + args args + want *session.UserAgent + }{ + { + name: "empty", + args: args{domain.UserAgent{}}, + }, + { + name: "fingerprint id and description", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + }, + }, + { + name: "with ip", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + IP: net.IPv4(1, 2, 3, 4), + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Ip: gu.Ptr("1.2.3.4"), + }, + }, + { + name: "with header", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Header: http.Header{ + "foo": []string{"foo", "bar"}, + "hello": []string{"world"}, + }, + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + "hello": {Values: []string{"world"}}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := userAgentToPb(tt.args.ua) + assert.Equal(t, tt.want, got) + }) + } +} + func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery { q, err := query.NewTextQuery(column, value, compare) require.NoError(t, err) @@ -510,3 +592,73 @@ func Test_userVerificationRequirementToDomain(t *testing.T) { }) } } + +func Test_userAgentToCommand(t *testing.T) { + type args struct { + userAgent *session.UserAgent + } + tests := []struct { + name string + args args + want *domain.UserAgent + }{ + { + name: "nil", + args: args{nil}, + want: nil, + }, + { + name: "all fields", + args: args{&session.UserAgent{ + FingerprintId: gu.Ptr("fp1"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: map[string]*session.UserAgent_HeaderValues{ + "hello": { + Values: []string{"foo", "bar"}, + }, + }, + }}, + want: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{ + "hello": []string{"foo", "bar"}, + }, + }, + }, + { + name: "invalid ip", + args: args{&session.UserAgent{ + FingerprintId: gu.Ptr("fp1"), + Ip: gu.Ptr("oops"), + Description: gu.Ptr("firefox"), + Header: map[string]*session.UserAgent_HeaderValues{ + "hello": { + Values: []string{"foo", "bar"}, + }, + }, + }}, + want: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: nil, + Description: gu.Ptr("firefox"), + Header: http.Header{ + "hello": []string{"foo", "bar"}, + }, + }, + }, + { + name: "nil fields", + args: args{&session.UserAgent{}}, + want: &domain.UserAgent{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := userAgentToCommand(tt.args.userAgent) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 9e6bf8328d..61f7c9184e 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -2,6 +2,8 @@ package command import ( "context" + "net" + "net/http" "testing" "time" @@ -358,7 +360,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), ), ), tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { @@ -401,7 +411,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), ), ), tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { @@ -444,8 +462,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate), - ), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "userID", testNow), @@ -523,8 +548,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate), - ), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "userID", testNow), diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index aba917fa24..59d8f7d3f3 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -2,6 +2,8 @@ package command import ( "context" + "net" + "net/http" "testing" "time" @@ -164,7 +166,15 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), ), eventFromEventPusher( session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, @@ -365,7 +375,15 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), ), eventFromEventPusher( session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, diff --git a/internal/command/session.go b/internal/command/session.go index a58c5d2d3a..caf3056f76 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -166,8 +166,8 @@ func (s *SessionCommands) Exec(ctx context.Context) error { return nil } -func (s *SessionCommands) Start(ctx context.Context) { - s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate)) +func (s *SessionCommands) Start(ctx context.Context, userAgent *domain.UserAgent) { + s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate, userAgent)) } func (s *SessionCommands) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error { @@ -280,7 +280,7 @@ func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Co return token, s.eventCommands, nil } -func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte) (set *SessionChanged, err error) { +func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte, userAgent *domain.UserAgent) (set *SessionChanged, err error) { sessionID, err := c.idGenerator.Next() if err != nil { return nil, err @@ -291,7 +291,7 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, met return nil, err } cmd := c.NewSessionCommands(cmds, sessionWriteModel) - cmd.Start(ctx) + cmd.Start(ctx, userAgent) return c.updateSession(ctx, cmd, metadata) } diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 57f3ba971c..048ed51e84 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -3,10 +3,13 @@ package command import ( "context" "io" + "net" + "net/http" "testing" "time" "github.com/golang/mock/gomock" + "github.com/muhlemmer/gu" "github.com/pquerna/otp/totp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -145,9 +148,10 @@ func TestCommands_CreateSession(t *testing.T) { tokenCreator func(sessionID string) (string, string, error) } type args struct { - ctx context.Context - checks []SessionCommand - metadata map[string][]byte + ctx context.Context + checks []SessionCommand + metadata map[string][]byte + userAgent *domain.UserAgent } type res struct { want *SessionChanged @@ -200,12 +204,26 @@ func TestCommands_CreateSession(t *testing.T) { }, args{ ctx: authz.NewMockContext("", "org1", ""), + userAgent: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, }, []expect{ expectFilter(), expectPush( eventPusherToEvents( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID", ), @@ -229,7 +247,7 @@ func TestCommands_CreateSession(t *testing.T) { idGenerator: tt.fields.idGenerator, sessionTokenCreator: tt.fields.tokenCreator, } - got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata) + got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata, tt.args.userAgent) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) @@ -278,7 +296,15 @@ func TestCommands_UpdateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -303,7 +329,15 @@ func TestCommands_UpdateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -868,7 +902,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -893,7 +935,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -922,7 +972,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID"), @@ -953,7 +1011,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID"), diff --git a/internal/domain/user_agent.go b/internal/domain/user_agent.go new file mode 100644 index 0000000000..ca72c6bec4 --- /dev/null +++ b/internal/domain/user_agent.go @@ -0,0 +1,17 @@ +package domain + +import ( + "net" + httplib "net/http" +) + +type UserAgent struct { + FingerprintID *string `json:"fingerprint_id,omitempty"` + IP net.IP `json:"ip,omitempty"` + Description *string `json:"description,omitempty"` + Header httplib.Header `json:"header,omitempty"` +} + +func (ua UserAgent) IsEmpty() bool { + return ua.FingerprintID == nil && len(ua.IP) == 0 && ua.Description == nil && ua.Header == nil +} diff --git a/internal/query/prepare_test.go b/internal/query/prepare_test.go index 242b387408..c9770c0dd1 100644 --- a/internal/query/prepare_test.go +++ b/internal/query/prepare_test.go @@ -54,7 +54,7 @@ func assertPrepare(t *testing.T, prepareFunc, expectedObject interface{}, sqlExp } return isErr(err) } - object, ok, didScan := execScan(&database.DB{DB: client}, builder, scan, errCheck) + object, ok, didScan := execScan(t, &database.DB{DB: client}, builder, scan, errCheck) if !ok { t.Error(object) return false @@ -168,7 +168,7 @@ var ( selectBuilderType = reflect.TypeOf(sq.SelectBuilder{}) ) -func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, errCheck checkErr) (object interface{}, ok bool, didScan bool) { +func execScan(t testing.TB, client *database.DB, builder sq.SelectBuilder, scan interface{}, errCheck checkErr) (object interface{}, ok bool, didScan bool) { scanType := reflect.TypeOf(scan) err := validateScan(scanType) if err != nil { @@ -177,7 +177,7 @@ func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, e stmt, args, err := builder.ToSql() if err != nil { - return fmt.Errorf("unexpeted error from sql builder: %w", err), false, false + return fmt.Errorf("unexpected error from sql builder: %w", err), false, false } //resultSet represents *sql.Row or *sql.Rows, @@ -199,6 +199,9 @@ func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, e // if scan(*sql.Row)... } else if scanType.In(0).AssignableTo(rowType) { err = client.QueryRow(func(r *sql.Row) error { + if r.Err() != nil { + return r.Err() + } didScan = true res = reflect.ValueOf(scan).Call([]reflect.Value{reflect.ValueOf(r)}) if err, ok := res[1].Interface().(error); ok { @@ -213,6 +216,9 @@ func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, e if err != nil { err, ok := errCheck(err) + if !ok { + t.Fatal(err) + } if didScan { return res[0].Interface(), ok, didScan } diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go index 7305d6a87c..f51d7c1b2a 100644 --- a/internal/query/projection/session.go +++ b/internal/query/projection/session.go @@ -14,27 +14,31 @@ import ( ) const ( - SessionsProjectionTable = "projections.sessions5" + SessionsProjectionTable = "projections.sessions6" - SessionColumnID = "id" - SessionColumnCreationDate = "creation_date" - SessionColumnChangeDate = "change_date" - SessionColumnSequence = "sequence" - SessionColumnState = "state" - SessionColumnResourceOwner = "resource_owner" - SessionColumnInstanceID = "instance_id" - SessionColumnCreator = "creator" - SessionColumnUserID = "user_id" - SessionColumnUserCheckedAt = "user_checked_at" - SessionColumnPasswordCheckedAt = "password_checked_at" - SessionColumnIntentCheckedAt = "intent_checked_at" - SessionColumnWebAuthNCheckedAt = "webauthn_checked_at" - SessionColumnWebAuthNUserVerified = "webauthn_user_verified" - SessionColumnTOTPCheckedAt = "totp_checked_at" - SessionColumnOTPSMSCheckedAt = "otp_sms_checked_at" - SessionColumnOTPEmailCheckedAt = "otp_email_checked_at" - SessionColumnMetadata = "metadata" - SessionColumnTokenID = "token_id" + SessionColumnID = "id" + SessionColumnCreationDate = "creation_date" + SessionColumnChangeDate = "change_date" + SessionColumnSequence = "sequence" + SessionColumnState = "state" + SessionColumnResourceOwner = "resource_owner" + SessionColumnInstanceID = "instance_id" + SessionColumnCreator = "creator" + SessionColumnUserID = "user_id" + SessionColumnUserCheckedAt = "user_checked_at" + SessionColumnPasswordCheckedAt = "password_checked_at" + SessionColumnIntentCheckedAt = "intent_checked_at" + SessionColumnWebAuthNCheckedAt = "webauthn_checked_at" + SessionColumnWebAuthNUserVerified = "webauthn_user_verified" + SessionColumnTOTPCheckedAt = "totp_checked_at" + SessionColumnOTPSMSCheckedAt = "otp_sms_checked_at" + SessionColumnOTPEmailCheckedAt = "otp_email_checked_at" + SessionColumnMetadata = "metadata" + SessionColumnTokenID = "token_id" + SessionColumnUserAgentFingerprintID = "user_agent_fingerprint_id" + SessionColumnUserAgentIP = "user_agent_ip" + SessionColumnUserAgentDescription = "user_agent_description" + SessionColumnUserAgentHeader = "user_agent_header" ) type sessionProjection struct { @@ -66,8 +70,16 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi crdb.NewColumn(SessionColumnOTPEmailCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()), crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()), crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(SessionColumnUserAgentFingerprintID, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(SessionColumnUserAgentIP, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(SessionColumnUserAgentDescription, crdb.ColumnTypeText, crdb.Nullable()), + crdb.NewColumn(SessionColumnUserAgentHeader, crdb.ColumnTypeJSONB, crdb.Nullable()), }, crdb.NewPrimaryKey(SessionColumnInstanceID, SessionColumnID), + crdb.WithIndex(crdb.NewIndex( + SessionColumnUserAgentFingerprintID+"_idx", + []string{SessionColumnUserAgentFingerprintID}, + )), ), ) p.StatementHandler = crdb.NewStatementHandler(ctx, config) @@ -152,19 +164,35 @@ func (p *sessionProjection) reduceSessionAdded(event eventstore.Event) (*handler return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Sfrgf", "reduce.wrong.event.type %s", session.AddedType) } - return crdb.NewCreateStatement( - e, - []handler.Column{ - handler.NewCol(SessionColumnID, e.Aggregate().ID), - handler.NewCol(SessionColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCol(SessionColumnCreationDate, e.CreationDate()), - handler.NewCol(SessionColumnChangeDate, e.CreationDate()), - handler.NewCol(SessionColumnResourceOwner, e.Aggregate().ResourceOwner), - handler.NewCol(SessionColumnState, domain.SessionStateActive), - handler.NewCol(SessionColumnSequence, e.Sequence()), - handler.NewCol(SessionColumnCreator, e.User), - }, - ), nil + cols := make([]handler.Column, 0, 12) + cols = append(cols, + handler.NewCol(SessionColumnID, e.Aggregate().ID), + handler.NewCol(SessionColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SessionColumnCreationDate, e.CreationDate()), + handler.NewCol(SessionColumnChangeDate, e.CreationDate()), + handler.NewCol(SessionColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SessionColumnState, domain.SessionStateActive), + handler.NewCol(SessionColumnSequence, e.Sequence()), + handler.NewCol(SessionColumnCreator, e.User), + ) + if e.UserAgent != nil { + cols = append(cols, + handler.NewCol(SessionColumnUserAgentFingerprintID, e.UserAgent.FingerprintID), + handler.NewCol(SessionColumnUserAgentDescription, e.UserAgent.Description), + ) + if e.UserAgent.IP != nil { + cols = append(cols, + handler.NewCol(SessionColumnUserAgentIP, e.UserAgent.IP.String()), + ) + } + if e.UserAgent.Header != nil { + cols = append(cols, + handler.NewJSONCol(SessionColumnUserAgentHeader, e.UserAgent.Header), + ) + } + } + + return crdb.NewCreateStatement(e, cols), nil } func (p *sessionProjection) reduceUserChecked(event eventstore.Event) (*handler.Statement, error) { diff --git a/internal/query/projection/session_test.go b/internal/query/projection/session_test.go index 8ac52b7484..e18f1ca59c 100644 --- a/internal/query/projection/session_test.go +++ b/internal/query/projection/session_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -31,7 +33,15 @@ func TestSessionProjection_reduces(t *testing.T) { session.AddedType, session.AggregateType, []byte(`{ - "domain": "domain" + "domain": "domain", + "user_agent": { + "fingerprint_id": "fp1", + "ip": "1.2.3.4", + "description": "firefox", + "header": { + "foo": ["bar"] + } + } }`), ), session.AddedEventMapper), }, @@ -43,7 +53,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.sessions5 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedStmt: "INSERT INTO projections.sessions6 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator, user_agent_fingerprint_id, user_agent_description, user_agent_ip, user_agent_header) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -53,6 +63,10 @@ func TestSessionProjection_reduces(t *testing.T) { domain.SessionStateActive, uint64(15), "editor-user", + gu.Ptr("fp1"), + gu.Ptr("firefox"), + "1.2.3.4", + []byte(`{"foo":["bar"]}`), }, }, }, @@ -79,7 +93,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -112,7 +126,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -145,7 +159,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -178,7 +192,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -210,7 +224,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -242,7 +256,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -276,7 +290,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -308,7 +322,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions5 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.sessions6 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -335,7 +349,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions5 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.sessions6 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -366,7 +380,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)", + expectedStmt: "UPDATE projections.sessions6 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)", expectedArgs: []interface{}{ nil, "agg-id", diff --git a/internal/query/session.go b/internal/query/session.go index c098d3d110..73ff5a9688 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" errs "errors" + "net" + "net/http" "time" sq "github.com/Masterminds/squirrel" @@ -38,6 +40,7 @@ type Session struct { OTPSMSFactor SessionOTPFactor OTPEmailFactor SessionOTPFactor Metadata map[string][]byte + UserAgent domain.UserAgent } type SessionUserFactor struct { @@ -163,6 +166,22 @@ var ( name: projection.SessionColumnTokenID, table: sessionsTable, } + SessionColumnUserAgentFingerprintID = Column{ + name: projection.SessionColumnUserAgentFingerprintID, + table: sessionsTable, + } + SessionColumnUserAgentIP = Column{ + name: projection.SessionColumnUserAgentIP, + table: sessionsTable, + } + SessionColumnUserAgentDescription = Column{ + name: projection.SessionColumnUserAgentDescription, + table: sessionsTable, + } + SessionColumnUserAgentHeader = Column{ + name: projection.SessionColumnUserAgentHeader, + table: sessionsTable, + } ) func (q *Queries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (session *Session, err error) { @@ -261,6 +280,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil SessionColumnOTPEmailCheckedAt.identifier(), SessionColumnMetadata.identifier(), SessionColumnToken.identifier(), + SessionColumnUserAgentFingerprintID.identifier(), + SessionColumnUserAgentIP.identifier(), + SessionColumnUserAgentDescription.identifier(), + SessionColumnUserAgentHeader.identifier(), ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). @@ -283,6 +306,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil otpEmailCheckedAt sql.NullTime metadata database.Map[[]byte] token sql.NullString + userAgentIP sql.NullString + userAgentHeader database.Map[[]string] ) err := row.Scan( @@ -307,6 +332,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil &otpEmailCheckedAt, &metadata, &token, + &session.UserAgent.FingerprintID, + &userAgentIP, + &session.UserAgent.Description, + &userAgentHeader, ) if err != nil { @@ -329,7 +358,11 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time session.Metadata = metadata + session.UserAgent.Header = http.Header(userAgentHeader) + if userAgentIP.Valid { + session.UserAgent.IP = net.ParseIP(userAgentIP.String) + } return session, token.String, nil } } diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index fa5209bdd3..1bae095ec6 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -6,10 +6,13 @@ import ( "database/sql/driver" "errors" "fmt" + "net" + "net/http" "regexp" "testing" sq "github.com/Masterminds/squirrel" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/domain" @@ -17,57 +20,61 @@ import ( ) var ( - expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions5.id,` + - ` projections.sessions5.creation_date,` + - ` projections.sessions5.change_date,` + - ` projections.sessions5.sequence,` + - ` projections.sessions5.state,` + - ` projections.sessions5.resource_owner,` + - ` projections.sessions5.creator,` + - ` projections.sessions5.user_id,` + - ` projections.sessions5.user_checked_at,` + + expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + + ` projections.sessions6.creation_date,` + + ` projections.sessions6.change_date,` + + ` projections.sessions6.sequence,` + + ` projections.sessions6.state,` + + ` projections.sessions6.resource_owner,` + + ` projections.sessions6.creator,` + + ` projections.sessions6.user_id,` + + ` projections.sessions6.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + ` projections.users8.resource_owner,` + - ` projections.sessions5.password_checked_at,` + - ` projections.sessions5.intent_checked_at,` + - ` projections.sessions5.webauthn_checked_at,` + - ` projections.sessions5.webauthn_user_verified,` + - ` projections.sessions5.totp_checked_at,` + - ` projections.sessions5.otp_sms_checked_at,` + - ` projections.sessions5.otp_email_checked_at,` + - ` projections.sessions5.metadata,` + - ` projections.sessions5.token_id` + - ` FROM projections.sessions5` + - ` LEFT JOIN projections.login_names2 ON projections.sessions5.user_id = projections.login_names2.user_id AND projections.sessions5.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions5.user_id = projections.users8_humans.user_id AND projections.sessions5.instance_id = projections.users8_humans.instance_id` + - ` LEFT JOIN projections.users8 ON projections.sessions5.user_id = projections.users8.id AND projections.sessions5.instance_id = projections.users8.instance_id` + + ` projections.sessions6.password_checked_at,` + + ` projections.sessions6.intent_checked_at,` + + ` projections.sessions6.webauthn_checked_at,` + + ` projections.sessions6.webauthn_user_verified,` + + ` projections.sessions6.totp_checked_at,` + + ` projections.sessions6.otp_sms_checked_at,` + + ` projections.sessions6.otp_email_checked_at,` + + ` projections.sessions6.metadata,` + + ` projections.sessions6.token_id,` + + ` projections.sessions6.user_agent_fingerprint_id,` + + ` projections.sessions6.user_agent_ip,` + + ` projections.sessions6.user_agent_description,` + + ` projections.sessions6.user_agent_header` + + ` FROM projections.sessions6` + + ` LEFT JOIN projections.login_names2 ON projections.sessions6.user_id = projections.login_names2.user_id AND projections.sessions6.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions6.user_id = projections.users8_humans.user_id AND projections.sessions6.instance_id = projections.users8_humans.instance_id` + + ` LEFT JOIN projections.users8 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) - expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions5.id,` + - ` projections.sessions5.creation_date,` + - ` projections.sessions5.change_date,` + - ` projections.sessions5.sequence,` + - ` projections.sessions5.state,` + - ` projections.sessions5.resource_owner,` + - ` projections.sessions5.creator,` + - ` projections.sessions5.user_id,` + - ` projections.sessions5.user_checked_at,` + + expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + + ` projections.sessions6.creation_date,` + + ` projections.sessions6.change_date,` + + ` projections.sessions6.sequence,` + + ` projections.sessions6.state,` + + ` projections.sessions6.resource_owner,` + + ` projections.sessions6.creator,` + + ` projections.sessions6.user_id,` + + ` projections.sessions6.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + ` projections.users8.resource_owner,` + - ` projections.sessions5.password_checked_at,` + - ` projections.sessions5.intent_checked_at,` + - ` projections.sessions5.webauthn_checked_at,` + - ` projections.sessions5.webauthn_user_verified,` + - ` projections.sessions5.totp_checked_at,` + - ` projections.sessions5.otp_sms_checked_at,` + - ` projections.sessions5.otp_email_checked_at,` + - ` projections.sessions5.metadata,` + + ` projections.sessions6.password_checked_at,` + + ` projections.sessions6.intent_checked_at,` + + ` projections.sessions6.webauthn_checked_at,` + + ` projections.sessions6.webauthn_user_verified,` + + ` projections.sessions6.totp_checked_at,` + + ` projections.sessions6.otp_sms_checked_at,` + + ` projections.sessions6.otp_email_checked_at,` + + ` projections.sessions6.metadata,` + ` COUNT(*) OVER ()` + - ` FROM projections.sessions5` + - ` LEFT JOIN projections.login_names2 ON projections.sessions5.user_id = projections.login_names2.user_id AND projections.sessions5.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions5.user_id = projections.users8_humans.user_id AND projections.sessions5.instance_id = projections.users8_humans.instance_id` + - ` LEFT JOIN projections.users8 ON projections.sessions5.user_id = projections.users8.id AND projections.sessions5.instance_id = projections.users8.instance_id` + + ` FROM projections.sessions6` + + ` LEFT JOIN projections.login_names2 ON projections.sessions6.user_id = projections.login_names2.user_id AND projections.sessions6.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions6.user_id = projections.users8_humans.user_id AND projections.sessions6.instance_id = projections.users8_humans.instance_id` + + ` LEFT JOIN projections.users8 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) sessionCols = []string{ @@ -92,6 +99,10 @@ var ( "otp_email_checked_at", "metadata", "token", + "user_agent_fingerprint_id", + "user_agent_ip", + "user_agent_description", + "user_agent_header", } sessionsCols = []string{ @@ -443,6 +454,10 @@ func Test_SessionPrepare(t *testing.T) { testNow, []byte(`{"key": "dmFsdWU="}`), "tokenID", + "fingerPrintID", + "1.2.3.4", + "agentDescription", + []byte(`{"foo":["foo","bar"]}`), }, ), }, @@ -483,6 +498,12 @@ func Test_SessionPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + IP: net.IPv4(1, 2, 3, 4), + Description: gu.Ptr("agentDescription"), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, }, }, { diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index c34ef52424..1966c178eb 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -35,6 +35,7 @@ const ( type AddedEvent struct { eventstore.BaseEvent `json:"-"` + UserAgent *domain.UserAgent `json:"user_agent,omitempty"` } func (e *AddedEvent) Data() interface{} { @@ -47,6 +48,7 @@ func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { func NewAddedEvent(ctx context.Context, aggregate *eventstore.Aggregate, + userAgent *domain.UserAgent, ) *AddedEvent { return &AddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -54,6 +56,7 @@ func NewAddedEvent(ctx context.Context, aggregate, AddedType, ), + UserAgent: userAgent, } } diff --git a/proto/zitadel/session/v2beta/session.proto b/proto/zitadel/session/v2beta/session.proto index ddbe143361..b0bfd4fb14 100644 --- a/proto/zitadel/session/v2beta/session.proto +++ b/proto/zitadel/session/v2beta/session.proto @@ -39,6 +39,7 @@ message Session { description: "\"custom key value list\""; } ]; + UserAgent user_agent = 7; } message Factors { @@ -131,3 +132,18 @@ message SearchQuery { message IDsQuery { repeated string ids = 1; } + +message UserAgent { + optional string fingerprint_id = 1; + optional string ip = 2; + optional string description = 3; + + // A header may have multiple values. + // In Go, headers are defined + // as map[string][]string, but protobuf + // doesn't allow this scheme. + message HeaderValues { + repeated string values = 1; + } + map header = 4; +} \ No newline at end of file diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index 745c430019..718a29381f 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -274,6 +274,7 @@ message CreateSessionRequest{ } ]; RequestChallenges challenges = 3; + UserAgent user_agent = 4; } message CreateSessionResponse{ From 53034a5fb1e9614f9bf5d6d35e6c76856b468bca Mon Sep 17 00:00:00 2001 From: mffap Date: Thu, 12 Oct 2023 15:08:38 +0200 Subject: [PATCH 02/48] docs(legal): onboarding support services (#6665) * docs(legal): onboarding support services * remove trainings, outline * wip * finish * call to action * Apply suggestions from code review Co-authored-by: Florian Forster --------- Co-authored-by: Florian Forster --- docs/docs/legal/onboarding-support.md | 87 +++++++++++++++++++++ docs/docs/legal/support-services.md | 5 +- docs/docs/support/trainings/application.md | 36 --------- docs/docs/support/trainings/introduction.md | 30 ------- docs/docs/support/trainings/project.md | 35 --------- docs/docs/support/trainings/recurring.md | 33 -------- docs/sidebars.js | 17 ++-- 7 files changed, 96 insertions(+), 147 deletions(-) create mode 100644 docs/docs/legal/onboarding-support.md delete mode 100644 docs/docs/support/trainings/application.md delete mode 100644 docs/docs/support/trainings/introduction.md delete mode 100644 docs/docs/support/trainings/project.md delete mode 100644 docs/docs/support/trainings/recurring.md diff --git a/docs/docs/legal/onboarding-support.md b/docs/docs/legal/onboarding-support.md new file mode 100644 index 0000000000..e9c76ac38e --- /dev/null +++ b/docs/docs/legal/onboarding-support.md @@ -0,0 +1,87 @@ +--- +title: Description of onboarding support services for ZITADEL +sidebar_label: Onboarding support +custom_edit_url: null +--- + +This annex of the [Framework Agreement](terms-of-service) describes the onboarding support services offered by us for our services. + +Last revised: October 12, 2023 + +Our onboarding support should help you, as a new customer, to get a better understanding on how to integrate ZITADEL into your solution, how to tackle the migration, and ensure a highly-available day-to-day operation. + +Onboarding support services can be offered to customers that enter a ZITADEL Cloud or a ZITADEL Enterprise subscription. + +If you intend to use the open source version exclusively then please join our community chat or Github. +Your questions might help other people in the community and will make our project better over time. + +Please [contact us](https://zitadel.com/contact) for a quote and to get started with onboarding support. +Below you will find topics covered and scope of the offered services. + +## Proof of value + +Within a short time-frame, f.e. 3 weeks, we can show the value of using our services and have the ability to establish the proof a of working setup for your most critical use cases. +We may offer to support you during an initial period to evaluate next steps. +Before the start of the period we may ask you to provide a description of your critical use cases and a high-level overview of your planned integration architecture. +During this period you should make sure that you have the necessary resources on your side to complete the proof of value. + +## Onboarding term + +With the onboarding support we provide the initial knowledge transfer to configure and operate ZITADEL. +During the term you will get direct access to our engineering team via [Technical Account Management](./support-services.md#technical-account-manager). +Duration is typically 3 months but this could vary depending on your requirements. + +We offer an onboarding term in combination with ZITADEL Enterprise subscriptions. + +### Topics covered + +Topics of the onboarding term may include: + +- Administration +- DevOps (Operation) +- Architecture +- Integration +- Migration +- Security Best Practices & Go-Live Checkup + +The scope will be tailored to your requirements. + +More details + +- IAM Configuration +- Walk-though all features +- Users / Manuals +- Authentication & Management APIs +- Validation of tokens +- Client integration best-practices +- Event types +- Database schemas and compute models +- Accessing database +- Observability (Logs, Errors, Metrics, Tracing) +- Operations best practices (Deployment, Backup, Networking etc.) +- Check prerequisites and architecture +- Troubleshoot installation and configuration of ZITADEL +- Troubleshoot and configuration connectivity to the database +- Functional testing of the ZITADEL instance + +
+ Out of scope +
    +
  • Performance testing
  • +
  • Setting up or maintaining backup storage
  • +
  • Running multiple ZITADEL instances on the same cluster
  • +
  • Integration into internal monitoring and alerting
  • +
  • Multi-cluster architecture deployments
  • +
  • DNS, Network and Firewall configuration
  • +
  • Customer-specific Kubernetes configuration needs
  • +
  • Non-production environments
  • +
  • Production deployment
  • +
  • Application-side coding, configuration, or tuning
  • + +
+
+ +## Continuous support + +After the onboarding phase has ended we will provide continuous support according to your subscription. +We can provide you with continued access to the technical account management in our Enterprise subscriptions. diff --git a/docs/docs/legal/support-services.md b/docs/docs/legal/support-services.md index 1c0f36b3fb..1a6f0a1555 100644 --- a/docs/docs/legal/support-services.md +++ b/docs/docs/legal/support-services.md @@ -10,7 +10,7 @@ This annex of the [Framework Agreement](terms-of-service) and the [Support Servi Support Services for products and services provided by ZITADEL is offered to customers according to the terms and conditions outlined in this document. The customer may purchase support services from ZITADEL (CAOS Ltd.) directly. -Last revised: March 15, 2023 +Last revised: October 6, 2023 ## Support Services @@ -82,7 +82,8 @@ Phone Support | +41 43 215 27 34 ZITADEL will enhance its support offering by providing eligible clients with a Technical Account Manager (TAM), who will perform the following tasks for up to the specified amount of time per week during the term of service: - Provide support and advice regarding best practices on platform, product and configuration covered by the applicable Support Services; -- Participate in review calls every other week at mutually agreed times addressing customer’s operational issues. +- Participate in review calls every other week at mutually agreed times addressing customer’s operational challenges or complex support requests; +- Walk-through of new features and customer feedback. We offer TAM services only bundled with specific subscription plans, and the option to add more TAM hours to these plans. If you require consulting for your projects, please request a quote via our [website](https://zitadel.com/contact). diff --git a/docs/docs/support/trainings/application.md b/docs/docs/support/trainings/application.md deleted file mode 100644 index cf313bb961..0000000000 --- a/docs/docs/support/trainings/application.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Application Support Trainings ---- - -## ZITADEL DevOps - -In this session your second level support and operations team will gain an understanding on how to extract relevant information for technical support questions and root cause analysis. We will also present our DevOps best practices and answer your questions. - -**Audience**: 2nd Level Support Staff, Operations -**Duration**: 0.5 day - -**Topics covered**: - -- Event types -- Database schemas and compute models -- Accessing database -- Observability (Logs, Errors, Metrics, Tracing) -- Operations best practices (Deployment, Backup, Networking etc.) -- Q&A - -## ZITADEL Administrator - -In this hands-on training your employees will get a complete overview of the system and learn how to configure and use ZITADEL. Your support staff will gain the required knowledge to provide user-support, while your solution owners gain an understanding about integration of clients. - -**Audience**: 1st / 2nd Level Support Staff, Solution Owner, QA Manager (optional) -**Duration**: 0.5 days - -**Topics covered**: - -- IAM Configuration -- Walk-though all features -- Users / Manuals -- APIs -- Validation of tokens -- Client integration best-practices -- Q&A diff --git a/docs/docs/support/trainings/introduction.md b/docs/docs/support/trainings/introduction.md deleted file mode 100644 index c597470f38..0000000000 --- a/docs/docs/support/trainings/introduction.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: ZITADEL Trainings -sidebar_label: Introduction ---- - -The following pages describe the the trainings provided by ZITADEL. These trainings are intended for onboarding and during the course of a Support Program. - -Training should be held as block-sessions with the relevant staff from your organization. - -## Onboarding Project - -You receive professional onboarding support from our engineers, who help you to setup and configure ZITADEL on your infrastructure. - -[More information](project) - -## Application Support Trainings - -With the application support trainings we provide the initial knowledge transfer to manage and support ZITADEL. The trainings are held as block-sessions with relevant staff from your organization. Prices are flat-fee, excl. expenses. - -* [ZITADEL DevOps](application#zitadel-devops) -* [ZITADEL Administrator](application#zitadel-administrator) - -## Recurring Trainings - -While you can benefit from a technical account manager during your term, these trainings are designed to onboard new staff or update staff about larger changes to the platform. Prices are flat-fee, excl. expenses. - -* [ZITADEL Support Refresher](recurring#zitadel-support-refresher) -* [ZITADEL Support Onboarding](recurring#zitadel-support-onboarding) - -In case you have any questions please [get in touch](https://zitadel.com/contact). diff --git a/docs/docs/support/trainings/project.md b/docs/docs/support/trainings/project.md deleted file mode 100644 index 35f3259b46..0000000000 --- a/docs/docs/support/trainings/project.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Onboarding Project ---- - -Effort required during depends on the complexity of your infrastructure and the overall setup. With a Multi-Zone Setup (excl. Multi-Region), support during this phase requires around 10-25h over 2 weeks. Actual effort is based on time and material. - -Scope of the project is agreed on individual basis. - -## In Scope - -- Check prerequisites and architecture -- Troubleshoot installation and configuration of ZITADEL -- Troubleshoot and configuration connectivity to the database -- Functional testing of the ZITADEL instance - -## Out of Scope - -- Running multiple ZITADEL instances on the same cluster -- Integration into internal monitoring and alerting -- Multi-cluster architecture deployments -- DNS, Network and Firewall configuration -- Customer-specific Kubernetes configuration needs -- Changes for specific environments -- Performance testing -- Production deployment -- Application-side coding, configuration, or tuning -- Changes or configuration on assets used in ZITADEL -- Setting up or maintaining backup storage - -## Prerequisites - -- Running Kubernetes with possibility to deploy to namespaces -- Inbound and outbound HTTP/2 traffic possible -- For being able to send SMS, we need a Twilio sender name, SID and an auth token -- ZITADEL also needs to connect to an email relay of your choice. We need the SMTP host, user and app key as well as the ZITADEL emails sender address and name. diff --git a/docs/docs/support/trainings/recurring.md b/docs/docs/support/trainings/recurring.md deleted file mode 100644 index 29982cf620..0000000000 --- a/docs/docs/support/trainings/recurring.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Recurring Trainings ---- - -## ZITADEL Support Refresher - -In this session you can refresh knowledge about existing and gain experience with new features of ZITADEL to keep the quality of your support high. We recommend an half day training per support staff. - -**Audience**: 1st / 2nd Level Support Staff, Solution Owner, QA Manager (optional) -**Duration**: 0.5 day / support staff - -**Topics covered**: - -* Walk-through new features -* Review of difficult support issues -* Review of customer feedback -* Q&A - -## ZITADEL Support Onboarding - -In this hands-on training new support staff will get an overview of the system and learn how to configure and use ZITADEL to provide support for users. - -**Audience**: 1st / 2nd Level Support Staff, Solution Owner, QA Manager (optional) -**Duration**: 0.5 days / support staff - -**Topics covered**: - -* Event types -* Accessing database -* Logs and Errors -* Validation of tokens -* Walk-through key features -* Q&A diff --git a/docs/sidebars.js b/docs/sidebars.js index 1c82d82852..a2ba179c80 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -425,6 +425,11 @@ module.exports = { items: [ "support/software-release-cycles-support", "support/troubleshooting", + { + type: 'link', + label: 'Support Service Descriptions', + href: '/legal/support-services', + }, { type: 'category', label: "Technical Advisory", @@ -440,17 +445,6 @@ module.exports = { }, ], }, - { - type: "category", - label: "Trainings", - collapsed: true, - items: [ - "support/trainings/introduction", - "support/trainings/application", - "support/trainings/recurring", - "support/trainings/project", - ], - }, ] }, ], @@ -697,6 +691,7 @@ module.exports = { "legal/cloud-service-description", "legal/service-level-description", "legal/support-services", + "legal/onboarding-support", ], }, { From b24e120c66081be2396b097bfada30e000d3296e Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Thu, 12 Oct 2023 09:12:22 -0600 Subject: [PATCH 03/48] fix: typo in verify email default text (#6694) Fix typo in Verify email default text Co-authored-by: Elio Bischof --- cmd/defaults.yaml | 2 +- proto/zitadel/admin.proto | 2 +- proto/zitadel/management.proto | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 891828941e..abceff1f69 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -735,7 +735,7 @@ DefaultInstance: PreHeader: Verify email Subject: Verify email Greeting: Hello {{.DisplayName}}, - Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email. + Text: A new email has been added. Please use the button below to verify your email. (Code {{.Code}}) If you din't add a new email, please ignore this email. ButtonText: Verify email - MessageTextType: VerifyPhone Language: en diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 620936f12b..ea183aa3f6 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -6837,7 +6837,7 @@ message SetDefaultVerifyEmailMessageTextRequest { string text = 6 [ (validate.rules).string = {max_bytes: 40000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" + example: "\"A new email has been added. Please use the button below to verify your email. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" max_length: 10000; } ]; diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 6770f77427..7741224fa7 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -10914,7 +10914,7 @@ message SetCustomVerifyEmailMessageTextRequest { string text = 6 [ (validate.rules).string = {max_bytes: 40000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" + example: "\"A new email has been added. Please use the button below to verify your email. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" max_length: 10000; } ]; From 831a21a6e221ac6c4cbad7a87726e39bebf1f34e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 16:51:50 +0000 Subject: [PATCH 04/48] chore(deps): bump node from 18-buster to 20-buster in /build (#6258) Bumps node from 18-buster to 20-buster. --- updated-dependencies: - dependency-name: node dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/workflow.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/workflow.Dockerfile b/build/workflow.Dockerfile index c9fb7e6c2b..db27daf91c 100644 --- a/build/workflow.Dockerfile +++ b/build/workflow.Dockerfile @@ -103,7 +103,7 @@ COPY --from=core-assets /go/src/github.com/zitadel/zitadel/internal ./internal # ####################################### # download console dependencies # ####################################### -FROM node:18-buster AS console-deps +FROM node:20-buster AS console-deps WORKDIR /zitadel/console @@ -115,7 +115,7 @@ RUN yarn install --frozen-lockfile # ####################################### # generate console client # ####################################### -FROM node:18-buster AS console-client +FROM node:20-buster AS console-client WORKDIR /zitadel/console From 5a9609ef29cb724f65f711f580917788fba728b7 Mon Sep 17 00:00:00 2001 From: cpli <34531687+gwimm@users.noreply.github.com> Date: Fri, 13 Oct 2023 07:31:23 +0000 Subject: [PATCH 05/48] feat(actions): add "zitadel/uuid" module (#6135) * feat: add "zitadel/uuid" module * feat(actions/uuid): add v1, v3, and v4 UUIDs * add namespaces and improve hash based functions * add docs --------- Co-authored-by: Florian Forster Co-authored-by: Livio Spring --- docs/docs/apis/actions/modules.md | 55 +++++++++++++++++ internal/actions/uuid_module.go | 83 ++++++++++++++++++++++++++ internal/api/oidc/client.go | 4 +- internal/api/ui/login/custom_action.go | 8 +-- 4 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 internal/actions/uuid_module.go diff --git a/docs/docs/apis/actions/modules.md b/docs/docs/apis/actions/modules.md index 2cc99a222e..6624ff9deb 100644 --- a/docs/docs/apis/actions/modules.md +++ b/docs/docs/apis/actions/modules.md @@ -51,3 +51,58 @@ The object has the following fields and methods: Returns the body as JSON object, or throws an error if the body is not a json object. - `text()` *string* Returns the body + +## UUID + +This module provides functionality to generate a UUID + +### Import + +```js + let uuid = require("zitadel/uuid") +``` + +### `uuid.vX()` function + +This function generates a UUID using [google/uuid](https://github.com/google/uuid). `vX` allows to define the UUID version: + +- `uuid.v1()` *string* + Generates a UUID version 1, based on date-time and MAC address +- `uuid.v3(namespace, data)` *string* + Generates a UUID version 3, based on the provided namespace using MD5 +- `uuid.v4()` *string* + Generates a UUID version 4, which is randomly generated +- `uuid.v5(namespace, data)` *string* + Generates a UUID version 5, based on the provided namespace using SHA1 + +#### Parameters + +- `namespace` *UUID*/*string* + Namespace to be used in the hashing function. Either provide one of defined [namespaces](#namespaces) or a string representing a UUID. +- `data` *[]byte*/*string* + data to be used in the hashing function. Possible types are []byte or string. + +### Namespaces + +The following predefined namespaces can be used for `uuid.v3` and `uuid.v5`: + +- `uuid.namespaceDNS` *UUID* + 6ba7b810-9dad-11d1-80b4-00c04fd430c8 +- `uuid.namespaceURL` *UUID* + 6ba7b811-9dad-11d1-80b4-00c04fd430c8 +- `uuid.namespaceOID` *UUID* + 6ba7b812-9dad-11d1-80b4-00c04fd430c8 +- `uuid.namespaceX500` *UUID* + 6ba7b814-9dad-11d1-80b4-00c04fd430c8 + +### Example +```js +let uuid = require("zitadel/uuid") +function setUUID(ctx, api) { + if (api.metadata === undefined) { + return; + } + + api.v1.user.appendMetadata('custom-id', uuid.v4()); +} +``` \ No newline at end of file diff --git a/internal/actions/uuid_module.go b/internal/actions/uuid_module.go new file mode 100644 index 0000000000..15d2992127 --- /dev/null +++ b/internal/actions/uuid_module.go @@ -0,0 +1,83 @@ +package actions + +import ( + "context" + + "github.com/dop251/goja" + "github.com/google/uuid" + "github.com/zitadel/logging" +) + +func WithUUID(ctx context.Context) Option { + return func(c *runConfig) { + c.modules["zitadel/uuid"] = func(runtime *goja.Runtime, module *goja.Object) { + requireUUID(ctx, runtime, module) + } + } +} + +func requireUUID(_ context.Context, runtime *goja.Runtime, module *goja.Object) { + o := module.Get("exports").(*goja.Object) + logging.OnError(o.Set("v1", inRuntime(uuid.NewUUID, runtime))).Warn("unable to set module") + logging.OnError(o.Set("v3", inRuntimeHash(uuid.NewMD5, runtime))).Warn("unable to set module") + logging.OnError(o.Set("v4", inRuntime(uuid.NewRandom, runtime))).Warn("unable to set module") + logging.OnError(o.Set("v5", inRuntimeHash(uuid.NewSHA1, runtime))).Warn("unable to set module") + logging.OnError(o.Set("namespaceDNS", uuid.NameSpaceDNS)).Warn("unable to set namespace") + logging.OnError(o.Set("namespaceURL", uuid.NameSpaceURL)).Warn("unable to set namespace") + logging.OnError(o.Set("namespaceOID", uuid.NameSpaceOID)).Warn("unable to set namespace") + logging.OnError(o.Set("namespaceX500", uuid.NameSpaceX500)).Warn("unable to set namespace") +} + +func inRuntime(function func() (uuid.UUID, error), runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value { + return func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) != 0 { + panic("invalid arg count") + } + + uuid, err := function() + if err != nil { + logging.WithError(err) + panic(err) + } + + return runtime.ToValue(uuid.String()) + } +} + +func inRuntimeHash(function func(uuid.UUID, []byte) uuid.UUID, runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value { + return func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) != 2 { + logging.WithFields("count", len(call.Arguments)).Debug("other than 2 args provided") + panic("invalid arg count") + } + + var err error + var namespace uuid.UUID + switch n := call.Arguments[0].Export().(type) { + case string: + namespace, err = uuid.Parse(n) + if err != nil { + logging.WithError(err).Debug("namespace failed parsing as UUID") + panic(err) + } + case uuid.UUID: + namespace = n + default: + logging.WithError(err).Debug("invalid type for namespace") + panic(err) + } + + var data []byte + switch d := call.Arguments[1].Export().(type) { + case string: + data = []byte(d) + case []byte: + data = d + default: + logging.WithError(err).Debug("invalid type for data") + panic(err) + } + + return runtime.ToValue(function(namespace, data).String()) + } +} diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 6a8dc6be6e..4bfbb6449a 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -564,7 +564,7 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, user *query.User, userGra apiFields, action.Script, action.Name, - append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -745,7 +745,7 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, userG apiFields, action.Script, action.Name, - append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 516f7bc3d0..22b3f72ccf 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -133,7 +133,7 @@ func (l *Login) runPostExternalAuthenticationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -206,7 +206,7 @@ func (l *Login) runPostInternalAuthenticationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -307,7 +307,7 @@ func (l *Login) runPreCreationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -365,7 +365,7 @@ func (l *Login) runPostCreationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { From 27e03120dce9b9fb7eb4f0d6ff6b2017f4be71a0 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 13 Oct 2023 14:31:39 +0300 Subject: [PATCH 06/48] fix(api): extend client_secret length for generic oauth and oidc providers to 1000 (#6722) --- proto/zitadel/admin.proto | 8 ++++---- proto/zitadel/management.proto | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index ea183aa3f6..4fce496fe2 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -4887,7 +4887,7 @@ message AddGenericOAuthProviderRequest { } ]; string client_secret = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret generated by the identity provider"; @@ -4954,7 +4954,7 @@ message UpdateGenericOAuthProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 4 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret will only be updated if provided"; @@ -5025,7 +5025,7 @@ message AddGenericOIDCProviderRequest { } ]; string client_secret = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "secret generated by the identity provider" @@ -5076,7 +5076,7 @@ message UpdateGenericOIDCProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 5 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "client secret will only be updated if provided"; diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 7741224fa7..9f2e35f2cc 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -11759,7 +11759,7 @@ message AddGenericOAuthProviderRequest { } ]; string client_secret = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret generated by the identity provider"; @@ -11826,7 +11826,7 @@ message UpdateGenericOAuthProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 4 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret will only be updated if provided"; @@ -11897,7 +11897,7 @@ message AddGenericOIDCProviderRequest { } ]; string client_secret = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "secret generated by the identity provider" @@ -11948,7 +11948,7 @@ message UpdateGenericOIDCProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 5 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "client secret will only be updated if provided"; From 95889cf576519e71d818fd0f1ed7452a20c5f3d6 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 13 Oct 2023 15:37:35 +0300 Subject: [PATCH 07/48] fix(api): use organization instead of organisation (#6720) * fix(api): use organization instead of organisation * fix test * docs: add deprecation notice * remove validation --- .../server/middleware/auth_interceptor.go | 40 ++++++++++++++----- internal/api/grpc/session/v2/session.go | 1 + internal/api/grpc/session/v2/session_test.go | 7 +++- .../protoc-gen-zitadel/zitadel.pb.go.tmpl | 4 +- .../v2beta/user_service_org.pb.zitadel.go | 12 ++++++ proto/zitadel/admin.proto | 2 +- proto/zitadel/management.proto | 4 +- proto/zitadel/object/v2beta/object.proto | 8 ++++ proto/zitadel/project.proto | 2 +- proto/zitadel/session/v2beta/session.proto | 8 +++- proto/zitadel/user/v2beta/user_service.proto | 10 ++++- 11 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index 96426e8577..d2a81203ea 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -33,12 +33,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, return nil, status.Error(codes.Unauthenticated, "auth header missing") } - var orgDomain string - orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID) - if o, ok := req.(OrganisationFromRequest); ok { - orgID = o.OrganisationFromRequest().ID - orgDomain = o.OrganisationFromRequest().Domain - } + orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req) ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod) if err != nil { return nil, err @@ -47,11 +42,38 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, return handler(ctxSetter(ctx), req) } -type OrganisationFromRequest interface { - OrganisationFromRequest() *Organisation +func orgIDAndDomainFromRequest(ctx context.Context, req interface{}) (id, domain string) { + orgID := grpc_util.GetHeader(ctx, http.ZitadelOrgID) + o, ok := req.(OrganizationFromRequest) + if !ok { + return orgID, "" + } + id = o.OrganizationFromRequest().ID + domain = o.OrganizationFromRequest().Domain + if id != "" || domain != "" { + return id, domain + } + // check if the deprecated organisation is used. + // to be removed before going GA (https://github.com/zitadel/zitadel/issues/6718) + id = o.OrganisationFromRequest().ID + domain = o.OrganisationFromRequest().Domain + if id != "" || domain != "" { + return id, domain + } + return orgID, domain } -type Organisation struct { +// Deprecated: will be removed in favor of OrganizationFromRequest (https://github.com/zitadel/zitadel/issues/6718) +type OrganisationFromRequest interface { + OrganisationFromRequest() *Organization +} + +type Organization struct { ID string Domain string } + +type OrganizationFromRequest interface { + OrganizationFromRequest() *Organization + OrganisationFromRequest +} diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index be6907ae08..f98983936d 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -216,6 +216,7 @@ func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor { LoginName: factor.LoginName, DisplayName: factor.DisplayName, OrganisationId: factor.ResourceOwner, + OrganizationId: factor.ResourceOwner, } } diff --git a/internal/api/grpc/session/v2/session_test.go b/internal/api/grpc/session/v2/session_test.go index ae33fab4c7..8422b675b5 100644 --- a/internal/api/grpc/session/v2/session_test.go +++ b/internal/api/grpc/session/v2/session_test.go @@ -10,10 +10,11 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/zitadel/internal/api/authz" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" @@ -151,6 +152,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, }, Metadata: map[string][]byte{"hello": []byte("world")}, @@ -167,6 +169,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, Password: &session.PasswordFactor{ VerifiedAt: timestamppb.New(past), @@ -186,6 +189,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, WebAuthN: &session.WebAuthNFactor{ VerifiedAt: timestamppb.New(past), @@ -206,6 +210,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, Totp: &session.TOTPFactor{ VerifiedAt: timestamppb.New(past), diff --git a/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl b/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl index bc0123c750..adb71c42ff 100644 --- a/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl +++ b/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl @@ -17,8 +17,8 @@ var {{.ServiceName}}_AuthMethods = authz.MethodMapping { } {{ range $m := .AuthContext}} -func (r *{{ $m.Name }}) OrganisationFromRequest() *middleware.Organisation { - return &middleware.Organisation{ +func (r *{{ $m.Name }}) OrganizationFromRequest() *middleware.Organization { + return &middleware.Organization{ ID: r{{$m.OrgMethod}}.GetOrgId(), Domain: r{{$m.OrgMethod}}.GetOrgDomain(), } diff --git a/pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go b/pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go new file mode 100644 index 0000000000..fe2509bd95 --- /dev/null +++ b/pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go @@ -0,0 +1,12 @@ +package user + +import "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" + +// OrganisationFromRequest implements deprecated [middleware.OrganisationFromRequest] interface. +// it will be removed before going GA (https://github.com/zitadel/zitadel/issues/6718) +func (r *AddHumanUserRequest) OrganisationFromRequest() *middleware.Organization { + return &middleware.Organization{ + ID: r.GetOrganisation().GetOrgId(), + Domain: r.GetOrganisation().GetOrgDomain(), + } +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 4fce496fe2..c1f32616b2 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -3763,7 +3763,7 @@ service AdminService { // Activates the "LoginDefaultOrg" feature by setting the flag to "true" // This is irreversible! - // Once activated, the login UI will use the settings of the default org (and not from the instance) if not organisation context is set + // Once activated, the login UI will use the settings of the default org (and not from the instance) if not organization context is set rpc ActivateFeatureLoginDefaultOrg(ActivateFeatureLoginDefaultOrgRequest) returns (ActivateFeatureLoginDefaultOrgResponse) { option (google.api.http) = { put: "/features/login_default_org" diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 9f2e35f2cc..6a17b3cf51 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -6806,7 +6806,7 @@ service ManagementService { }; } - // Add a new Azure AD identity provider in the organisation + // Add a new Azure AD identity provider in the organization rpc AddAzureADProvider(AddAzureADProviderRequest) returns (AddAzureADProviderResponse) { option (google.api.http) = { post: "/idps/azure" @@ -6824,7 +6824,7 @@ service ManagementService { }; } - // Change an existing Azure AD identity provider in the organisation + // Change an existing Azure AD identity provider in the organization rpc UpdateAzureADProvider(UpdateAzureADProviderRequest) returns (UpdateAzureADProviderResponse) { option (google.api.http) = { put: "/idps/azure/{id}" diff --git a/proto/zitadel/object/v2beta/object.proto b/proto/zitadel/object/v2beta/object.proto index 75f82d98ae..c7c68f31ce 100644 --- a/proto/zitadel/object/v2beta/object.proto +++ b/proto/zitadel/object/v2beta/object.proto @@ -8,6 +8,7 @@ import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +// Deprecated: use Organization message Organisation { oneof org { string org_id = 1; @@ -15,6 +16,13 @@ message Organisation { } } +message Organization { + oneof org { + string org_id = 1; + string org_domain = 2; + } +} + message RequestContext { oneof resource_owner { string org_id = 1; diff --git a/proto/zitadel/project.proto b/proto/zitadel/project.proto index e050ba3dfa..b487a2dd34 100644 --- a/proto/zitadel/project.proto +++ b/proto/zitadel/project.proto @@ -48,7 +48,7 @@ message GrantedProject { ]; string granted_org_name = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"Some Organisation\"" + example: "\"Some Organization\"" } ]; repeated string granted_role_keys = 4 [ diff --git a/proto/zitadel/session/v2beta/session.proto b/proto/zitadel/session/v2beta/session.proto index b0bfd4fb14..08ef97e215 100644 --- a/proto/zitadel/session/v2beta/session.proto +++ b/proto/zitadel/session/v2beta/session.proto @@ -73,9 +73,15 @@ message UserFactor { description: "\"display name of the checked user\""; } ]; + // deprecated: use organization_id, will be remove before GA (https://github.com/zitadel/zitadel/issues/6718) string organisation_id = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "\"organisation id of the checked user\""; + description: "\"organization id of the checked user; deprecated: use organization_id\""; + } + ]; + string organization_id = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"organization id of the checked user\""; } ]; } diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 01abeceed4..36517dc4c5 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -119,7 +119,7 @@ service UserService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { permission: "user.write" - org_field: "organisation" + org_field: "organization" } http_response: { success_code: 201 @@ -655,7 +655,13 @@ message AddHumanUserRequest{ example: "\"minnie-mouse\""; } ]; - zitadel.object.v2beta.Organisation organisation = 3; + // deprecated: use organization (if both are set, organization will take precedence) + zitadel.object.v2beta.Organisation organisation = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "deprecated: use organization (if both are set, organization will take precedence)" + } + ]; + zitadel.object.v2beta.Organization organization = 11; SetHumanProfile profile = 4 [ (validate.rules).message.required = true, (google.api.field_behavior) = REQUIRED From 0af1c65c4ca0f4917b20f9502f99f66630a62951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 13 Oct 2023 16:11:20 +0300 Subject: [PATCH 08/48] fix: allow unused keys in hasher config (#6724) --- internal/crypto/passwap.go | 2 +- internal/crypto/passwap_test.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index a5a293a449..479d5731e4 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -147,7 +147,7 @@ func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string, func (c *HasherConfig) decodeParams(dst any) error { decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - ErrorUnused: true, + ErrorUnused: false, ErrorUnset: true, Result: dst, }) diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index b557ca4a5c..0538ac631a 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -379,7 +379,10 @@ func TestHasherConfig_decodeParams(t *testing.T) { "b": 2, "c": 3, }, - wantErr: true, + want: dst{ + A: 1, + B: 2, + }, }, { name: "unset", From ce719a3fa4b2559c234f545b3795f51d263ae88f Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 13 Oct 2023 17:45:38 +0300 Subject: [PATCH 09/48] fix(notification): get origin from all relevant events and fix nil pointer (#6726) --- internal/notification/handlers/origin.go | 9 ++++--- internal/notification/types/user_email.go | 2 +- internal/notification/types/user_phone.go | 3 ++- internal/repository/session/session.go | 18 ++++++++----- internal/repository/user/human_mfa_otp.go | 33 ++++++++++++++++------- internal/repository/user/human_phone.go | 19 ++++++++----- 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/internal/notification/handlers/origin.go b/internal/notification/handlers/origin.go index a807edd2d3..915e1fed1e 100644 --- a/internal/notification/handlers/origin.go +++ b/internal/notification/handlers/origin.go @@ -2,9 +2,10 @@ package handlers import ( "context" - "fmt" "net/url" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/errors" @@ -18,11 +19,13 @@ type OriginEvent interface { } func (n *NotificationQueries) Origin(ctx context.Context, e eventstore.Event) (context.Context, error) { + var origin string originEvent, ok := e.(OriginEvent) if !ok { - return ctx, errors.ThrowInternal(fmt.Errorf("event of type %T doesn't implement OriginEvent", e), "NOTIF-3m9fs", "Errors.Internal") + logging.Errorf("event of type %T doesn't implement OriginEvent", e) + } else { + origin = originEvent.TriggerOrigin() } - origin := originEvent.TriggerOrigin() if origin != "" { originURL, err := url.Parse(origin) if err != nil { diff --git a/internal/notification/types/user_email.go b/internal/notification/types/user_email.go index 7cb3498e4d..152b630769 100644 --- a/internal/notification/types/user_email.go +++ b/internal/notification/types/user_email.go @@ -33,7 +33,7 @@ func generateEmail( if err != nil { return err } - if emailChannels.Len() == 0 { + if emailChannels == nil || emailChannels.Len() == 0 { return errors.ThrowPreconditionFailed(nil, "MAIL-83nof", "Errors.Notification.Channels.NotPresent") } return emailChannels.HandleMessage(message) diff --git a/internal/notification/types/user_phone.go b/internal/notification/types/user_phone.go index 50b7f86375..6eb3314cda 100644 --- a/internal/notification/types/user_phone.go +++ b/internal/notification/types/user_phone.go @@ -4,6 +4,7 @@ import ( "context" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/notification/messages" @@ -21,7 +22,7 @@ func generateSms( number := "" smsChannels, twilioConfig, err := channels.SMS(ctx) logging.OnError(err).Error("could not create sms channel") - if smsChannels.Len() == 0 { + if smsChannels == nil || smsChannels.Len() == 0 { return errors.ThrowPreconditionFailed(nil, "PHONE-w8nfow", "Errors.Notification.Channels.NotPresent") } if err == nil { diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index 1966c178eb..106b205183 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -312,9 +312,10 @@ func NewTOTPCheckedEvent( type OTPSMSChallengedEvent struct { eventstore.BaseEvent `json:"-"` - Code *crypto.CryptoValue `json:"code"` - Expiry time.Duration `json:"expiry"` - CodeReturned bool `json:"codeReturned,omitempty"` + Code *crypto.CryptoValue `json:"code"` + Expiry time.Duration `json:"expiry"` + CodeReturned bool `json:"codeReturned,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` } func (e *OTPSMSChallengedEvent) Data() interface{} { @@ -329,6 +330,10 @@ func (e *OTPSMSChallengedEvent) SetBaseEvent(base *eventstore.BaseEvent) { e.BaseEvent = *base } +func (e *OTPSMSChallengedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + func NewOTPSMSChallengedEvent( ctx context.Context, aggregate *eventstore.Aggregate, @@ -342,9 +347,10 @@ func NewOTPSMSChallengedEvent( aggregate, OTPSMSChallengedType, ), - Code: code, - Expiry: expiry, - CodeReturned: codeReturned, + Code: code, + Expiry: expiry, + CodeReturned: codeReturned, + TriggeredAtOrigin: http.ComposedOrigin(ctx), } } diff --git a/internal/repository/user/human_mfa_otp.go b/internal/repository/user/human_mfa_otp.go index 52d5ad7a0c..d3dd2824cd 100644 --- a/internal/repository/user/human_mfa_otp.go +++ b/internal/repository/user/human_mfa_otp.go @@ -5,6 +5,7 @@ import ( "encoding/json" "time" + "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/crypto" @@ -279,8 +280,9 @@ func NewHumanOTPSMSRemovedEvent( type HumanOTPSMSCodeAddedEvent struct { eventstore.BaseEvent `json:"-"` - Code *crypto.CryptoValue `json:"code,omitempty"` - Expiry time.Duration `json:"expiry,omitempty"` + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` *AuthRequestInfo } @@ -296,6 +298,10 @@ func (e *HumanOTPSMSCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) { e.BaseEvent = *event } +func (e *HumanOTPSMSCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + func NewHumanOTPSMSCodeAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, @@ -309,9 +315,10 @@ func NewHumanOTPSMSCodeAddedEvent( aggregate, HumanOTPSMSCodeAddedType, ), - Code: code, - Expiry: expiry, - AuthRequestInfo: info, + Code: code, + Expiry: expiry, + TriggeredAtOrigin: http.ComposedOrigin(ctx), + AuthRequestInfo: info, } } @@ -473,8 +480,9 @@ func NewHumanOTPEmailRemovedEvent( type HumanOTPEmailCodeAddedEvent struct { eventstore.BaseEvent `json:"-"` - Code *crypto.CryptoValue `json:"code,omitempty"` - Expiry time.Duration `json:"expiry,omitempty"` + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` *AuthRequestInfo } @@ -490,6 +498,10 @@ func (e *HumanOTPEmailCodeAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) e.BaseEvent = *event } +func (e *HumanOTPEmailCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + func NewHumanOTPEmailCodeAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, @@ -503,9 +515,10 @@ func NewHumanOTPEmailCodeAddedEvent( aggregate, HumanOTPEmailCodeAddedType, ), - Code: code, - Expiry: expiry, - AuthRequestInfo: info, + Code: code, + Expiry: expiry, + AuthRequestInfo: info, + TriggeredAtOrigin: http.ComposedOrigin(ctx), } } diff --git a/internal/repository/user/human_phone.go b/internal/repository/user/human_phone.go index c6586c54e5..135b8f899b 100644 --- a/internal/repository/user/human_phone.go +++ b/internal/repository/user/human_phone.go @@ -5,6 +5,7 @@ import ( "encoding/json" "time" + "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" @@ -149,9 +150,10 @@ func HumanPhoneVerificationFailedEventMapper(event *repository.Event) (eventstor type HumanPhoneCodeAddedEvent struct { eventstore.BaseEvent `json:"-"` - Code *crypto.CryptoValue `json:"code,omitempty"` - Expiry time.Duration `json:"expiry,omitempty"` - CodeReturned bool `json:"code_returned,omitempty"` + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + CodeReturned bool `json:"code_returned,omitempty"` + TriggeredAtOrigin string `json:"triggerOrigin,omitempty"` } func (e *HumanPhoneCodeAddedEvent) Data() interface{} { @@ -162,6 +164,10 @@ func (e *HumanPhoneCodeAddedEvent) UniqueConstraints() []*eventstore.EventUnique return nil } +func (e *HumanPhoneCodeAddedEvent) TriggerOrigin() string { + return e.TriggeredAtOrigin +} + func NewHumanPhoneCodeAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, @@ -183,9 +189,10 @@ func NewHumanPhoneCodeAddedEventV2( aggregate, HumanPhoneCodeAddedType, ), - Code: code, - Expiry: expiry, - CodeReturned: codeReturned, + Code: code, + Expiry: expiry, + CodeReturned: codeReturned, + TriggeredAtOrigin: http.ComposedOrigin(ctx), } } From cb0a0f996e0771a30dd1b2d6a81d78782aa7fc94 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Mon, 16 Oct 2023 10:49:02 +0300 Subject: [PATCH 10/48] fix(api): add remove otp sms and email to management api (#6721) * fix(api): add remove otp sms and email to management api * fix(console): remove otpsms and otpemail from user --------- Co-authored-by: peintnermax --- .../auth-user-mfa/auth-user-mfa.component.ts | 4 +- .../user-mfa/user-mfa.component.ts | 30 +++++++ console/src/app/services/mgmt.service.ts | 16 ++++ internal/api/grpc/management/user.go | 20 +++++ proto/zitadel/management.proto | 80 ++++++++++++++++++- 5 files changed, 147 insertions(+), 3 deletions(-) diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts index 6a81838c44..c4a3d64033 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts @@ -157,7 +157,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy { this.service .removeMyAuthFactorOTPEmail() .then(() => { - this.toast.showInfo('USER.TOAST.U2FREMOVED', true); + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); this.cleanupList(); this.getMFAs(); @@ -169,7 +169,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy { this.service .removeMyAuthFactorOTPSMS() .then(() => { - this.toast.showInfo('USER.TOAST.U2FREMOVED', true); + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); this.cleanupList(); this.getMFAs(); diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts index c9be008e01..29418e80fe 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts @@ -102,6 +102,36 @@ export class UserMfaComponent implements OnInit, OnDestroy { .catch((error) => { this.toast.showError(error); }); + } else if (factor.otpEmail) { + this.mgmtUserService + .removeHumanAuthFactorOTPEmail(this.user.id) + .then(() => { + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); + + const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpEmail); + if (index > -1) { + this.dataSource.data.splice(index, 1); + } + this.getMFAs(); + }) + .catch((error) => { + this.toast.showError(error); + }); + } else if (factor.otpSms) { + this.mgmtUserService + .removeHumanAuthFactorOTPSMS(this.user.id) + .then(() => { + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); + + const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpSms); + if (index > -1) { + this.dataSource.data.splice(index, 1); + } + this.getMFAs(); + }) + .catch((error) => { + this.toast.showError(error); + }); } } }); diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index c80e9e9b49..f2389a25e9 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -322,8 +322,12 @@ import { RemoveCustomLabelPolicyLogoDarkResponse, RemoveCustomLabelPolicyLogoRequest, RemoveCustomLabelPolicyLogoResponse, + RemoveHumanAuthFactorOTPEmailRequest, + RemoveHumanAuthFactorOTPEmailResponse, RemoveHumanAuthFactorOTPRequest, RemoveHumanAuthFactorOTPResponse, + RemoveHumanAuthFactorOTPSMSRequest, + RemoveHumanAuthFactorOTPSMSResponse, RemoveHumanAuthFactorU2FRequest, RemoveHumanAuthFactorU2FResponse, RemoveHumanLinkedIDPRequest, @@ -1805,6 +1809,18 @@ export class ManagementService { return this.grpcService.mgmt.removeHumanAuthFactorU2F(req, null).then((resp) => resp.toObject()); } + public removeHumanAuthFactorOTPSMS(userId: string): Promise { + const req = new RemoveHumanAuthFactorOTPSMSRequest(); + req.setUserId(userId); + return this.grpcService.mgmt.removeHumanAuthFactorOTPSMS(req, null).then((resp) => resp.toObject()); + } + + public removeHumanAuthFactorOTPEmail(userId: string): Promise { + const req = new RemoveHumanAuthFactorOTPEmailRequest(); + req.setUserId(userId); + return this.grpcService.mgmt.removeHumanAuthFactorOTPEmail(req, null).then((resp) => resp.toObject()); + } + public updateHumanProfile( userId: string, firstName?: string, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 804a905210..2cc4fe21de 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -646,6 +646,26 @@ func (s *Server) RemoveHumanAuthFactorU2F(ctx context.Context, req *mgmt_pb.Remo }, nil } +func (s *Server) RemoveHumanAuthFactorOTPSMS(ctx context.Context, req *mgmt_pb.RemoveHumanAuthFactorOTPSMSRequest) (*mgmt_pb.RemoveHumanAuthFactorOTPSMSResponse, error) { + objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.RemoveHumanAuthFactorOTPSMSResponse{ + Details: obj_grpc.DomainToChangeDetailsPb(objectDetails), + }, nil +} + +func (s *Server) RemoveHumanAuthFactorOTPEmail(ctx context.Context, req *mgmt_pb.RemoveHumanAuthFactorOTPEmailRequest) (*mgmt_pb.RemoveHumanAuthFactorOTPEmailResponse, error) { + objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.RemoveHumanAuthFactorOTPEmailResponse{ + Details: obj_grpc.DomainToChangeDetailsPb(objectDetails), + }, nil +} + func (s *Server) ListHumanPasswordless(ctx context.Context, req *mgmt_pb.ListHumanPasswordlessRequest) (*mgmt_pb.ListHumanPasswordlessResponse, error) { query := new(query.UserAuthMethodSearchQueries) err := query.AppendUserIDQuery(req.UserId) diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 6a17b3cf51..3c485f7a92 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -1255,7 +1255,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Remove Multi-Factor OTP"; - description: "Remove the configured One-Time-Password (OTP) as a factor from the user. OTP is an authentication app, like Authy or Google/Microsoft Authenticator.." + description: "Remove the configured One-Time-Password (OTP) as a factor from the user. OTP is an authentication app, like Authy or Google/Microsoft Authenticator." tags: "Users"; tags: "User Human"; responses: { @@ -1306,6 +1306,68 @@ service ManagementService { }; } + rpc RemoveHumanAuthFactorOTPSMS(RemoveHumanAuthFactorOTPSMSRequest) returns (RemoveHumanAuthFactorOTPSMSResponse) { + option (google.api.http) = { + delete: "/users/{user_id}/auth_factors/otp_sms" + }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Remove Multi-Factor OTP SMS"; + description: "Remove the configured One-Time-Password (OTP) SMS as a factor from the user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward." + tags: "Users"; + tags: "User Human"; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get a user from another organization include the header. Make sure the requesting user has permission in the requested organization."; + type: STRING, + required: false; + }; + }; + }; + } + + rpc RemoveHumanAuthFactorOTPEmail(RemoveHumanAuthFactorOTPEmailRequest) returns (RemoveHumanAuthFactorOTPEmailResponse) { + option (google.api.http) = { + delete: "/users/{user_id}/auth_factors/otp_email" + }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Remove Multi-Factor OTP SMS"; + description: "Remove the configured One-Time-Password (OTP) Email as a factor from the user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward." + tags: "Users"; + tags: "User Human"; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get a user from another organization include the header. Make sure the requesting user has permission in the requested organization."; + type: STRING, + required: false; + }; + }; + }; + } + rpc ListHumanPasswordless(ListHumanPasswordlessRequest) returns (ListHumanPasswordlessResponse) { option (google.api.http) = { post: "/users/{user_id}/passwordless/_search" @@ -8246,6 +8308,22 @@ message RemoveHumanAuthFactorU2FResponse { zitadel.v1.ObjectDetails details = 1; } +message RemoveHumanAuthFactorOTPSMSRequest { + string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message RemoveHumanAuthFactorOTPSMSResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message RemoveHumanAuthFactorOTPEmailRequest { + string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message RemoveHumanAuthFactorOTPEmailResponse { + zitadel.v1.ObjectDetails details = 1; +} + message ListHumanPasswordlessRequest { string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } From 7b91d90eb2f6ba97e519bef6a0fad639a0b42444 Mon Sep 17 00:00:00 2001 From: Christoph Schmatzler Date: Mon, 16 Oct 2023 11:59:55 +0200 Subject: [PATCH 11/48] docs: fix environment variable name for steps (#6728) The yaml schema has a `Machine` object nested inside another one, which was improperly represented in the corresponding environment variable. Signed-off-by: Christoph Schmatzler Co-authored-by: Elio Bischof --- cmd/setup/steps.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/setup/steps.yaml b/cmd/setup/steps.yaml index 1497b7be4a..c585c535b2 100644 --- a/cmd/setup/steps.yaml +++ b/cmd/setup/steps.yaml @@ -35,8 +35,8 @@ FirstInstance: # If FirstInstance.Org.Machine.Machine is defined, a service user is created with the IAM_OWNER role, not a human user. Machine: Machine: - Username: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_USERNAME - Name: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_NAME + Username: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME + Name: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME MachineKey: # date format: 2023-01-01T00:00:00Z ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_EXPIRATIONDATE From bb1994c3181b02564dac1f30cfa073785933d368 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 17 Oct 2023 15:01:47 +0200 Subject: [PATCH 12/48] fix: origin from proxies (#6738) * fix: origin from proxies * test multiple forwarded header values --- .../api/http/middleware/origin_interceptor.go | 85 +++--------- .../middleware/origin_interceptor_test.go | 122 ++++++++++++++++++ 2 files changed, 143 insertions(+), 64 deletions(-) create mode 100644 internal/api/http/middleware/origin_interceptor_test.go diff --git a/internal/api/http/middleware/origin_interceptor.go b/internal/api/http/middleware/origin_interceptor.go index 2cf9a644f5..da03145ab0 100644 --- a/internal/api/http/middleware/origin_interceptor.go +++ b/internal/api/http/middleware/origin_interceptor.go @@ -3,7 +3,6 @@ package middleware import ( "fmt" "net/http" - "net/url" "github.com/muhlemmer/httpforwarded" "github.com/zitadel/logging" @@ -24,74 +23,32 @@ func OriginHandler(next http.Handler) http.Handler { } func composeOrigin(r *http.Request) string { - if origin, err := originFromForwardedHeader(r); err != nil { - logging.OnError(err).Debug("failed to build origin from forwarded header, trying x-forwarded-* headers") - } else { - return origin + var proto, host string + fwd, fwdErr := httpforwarded.ParseFromRequest(r) + if fwdErr == nil { + proto = oldestForwardedValue(fwd, "proto") + host = oldestForwardedValue(fwd, "host") } - if origin, err := originFromXForwardedHeaders(r); err != nil { - logging.OnError(err).Debug("failed to build origin from x-forwarded-* headers, using host header") - } else { - return origin + if proto == "" { + proto = r.Header.Get("X-Forwarded-Proto") } - scheme := "https" - if r.TLS == nil { - scheme = "http" - } - return fmt.Sprintf("%s://%s", scheme, r.Host) -} - -func originFromForwardedHeader(r *http.Request) (string, error) { - fwd, err := httpforwarded.ParseFromRequest(r) - if err != nil { - return "", err - } - var fwdProto, fwdHost, fwdPort string - if fwdProto = mostRecentValue(fwd, "proto"); fwdProto == "" { - return "", fmt.Errorf("no proto in forwarded header") - } - if fwdHost = mostRecentValue(fwd, "host"); fwdHost == "" { - return "", fmt.Errorf("no host in forwarded header") - } - fwdPort, foundFwdFor := extractPort(mostRecentValue(fwd, "for")) - if !foundFwdFor { - return "", fmt.Errorf("no for in forwarded header") - } - o := fmt.Sprintf("%s://%s", fwdProto, fwdHost) - if fwdPort != "" { - o += ":" + fwdPort - } - return o, nil -} - -func originFromXForwardedHeaders(r *http.Request) (string, error) { - scheme := r.Header.Get("X-Forwarded-Proto") - if scheme == "" { - return "", fmt.Errorf("no X-Forwarded-Proto header") - } - host := r.Header.Get("X-Forwarded-Host") if host == "" { - return "", fmt.Errorf("no X-Forwarded-Host header") + host = r.Header.Get("X-Forwarded-Host") } - return fmt.Sprintf("%s://%s", scheme, host), nil + if proto == "" { + if r.TLS == nil { + proto = "http" + } else { + proto = "https" + } + } + if host == "" { + host = r.Host + } + return fmt.Sprintf("%s://%s", proto, host) } -func extractPort(raw string) (string, bool) { - if u, ok := parseURL(raw); ok { - return u.Port(), ok - } - return "", false -} - -func parseURL(raw string) (*url.URL, bool) { - if raw == "" { - return nil, false - } - u, err := url.Parse(raw) - return u, err == nil -} - -func mostRecentValue(forwarded map[string][]string, key string) string { +func oldestForwardedValue(forwarded map[string][]string, key string) string { if forwarded == nil { return "" } @@ -99,5 +56,5 @@ func mostRecentValue(forwarded map[string][]string, key string) string { if len(values) == 0 { return "" } - return values[len(values)-1] + return values[0] } diff --git a/internal/api/http/middleware/origin_interceptor_test.go b/internal/api/http/middleware/origin_interceptor_test.go new file mode 100644 index 0000000000..e8e4f306b8 --- /dev/null +++ b/internal/api/http/middleware/origin_interceptor_test.go @@ -0,0 +1,122 @@ +package middleware + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_composeOrigin(t *testing.T) { + type args struct { + h http.Header + } + tests := []struct { + name string + args args + want string + }{{ + name: "no proxy headers", + want: "http://host.header", + }, { + name: "forwarded proto", + args: args{ + h: http.Header{ + "Forwarded": []string{"proto=https"}, + }, + }, + want: "https://host.header", + }, { + name: "forwarded host", + args: args{ + h: http.Header{ + "Forwarded": []string{"host=forwarded.host"}, + }, + }, + want: "http://forwarded.host", + }, { + name: "forwarded proto and host", + args: args{ + h: http.Header{ + "Forwarded": []string{"proto=https;host=forwarded.host"}, + }, + }, + want: "https://forwarded.host", + }, { + name: "forwarded proto and host with multiple complete entries", + args: args{ + h: http.Header{ + "Forwarded": []string{"proto=https;host=forwarded.host, proto=http;host=forwarded.host2"}, + }, + }, + want: "https://forwarded.host", + }, { + name: "forwarded proto and host with multiple incomplete entries", + args: args{ + h: http.Header{ + "Forwarded": []string{"proto=https;host=forwarded.host, proto=http"}, + }, + }, + want: "https://forwarded.host", + }, { + name: "forwarded proto and host with incomplete entries in different values", + args: args{ + h: http.Header{ + "Forwarded": []string{"proto=http", "proto=https;host=forwarded.host", "proto=http"}, + }, + }, + want: "http://forwarded.host", + }, { + name: "x-forwarded-proto", + args: args{ + h: http.Header{ + "X-Forwarded-Proto": []string{"https"}, + }, + }, + want: "https://host.header", + }, { + name: "x-forwarded-host", + args: args{ + h: http.Header{ + "X-Forwarded-Host": []string{"x-forwarded.host"}, + }, + }, + want: "http://x-forwarded.host", + }, { + name: "x-forwarded-proto and x-forwarded-host", + args: args{ + h: http.Header{ + "X-Forwarded-Proto": []string{"https"}, + "X-Forwarded-Host": []string{"x-forwarded.host"}, + }, + }, + want: "https://x-forwarded.host", + }, { + name: "forwarded host and x-forwarded-host", + args: args{ + h: http.Header{ + "Forwarded": []string{"host=forwarded.host"}, + "X-Forwarded-Host": []string{"x-forwarded.host"}, + }, + }, + want: "http://forwarded.host", + }, { + name: "forwarded host and x-forwarded-proto", + args: args{ + h: http.Header{ + "Forwarded": []string{"host=forwarded.host"}, + "X-Forwarded-Proto": []string{"https"}, + }, + }, + want: "https://forwarded.host", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, composeOrigin(&http.Request{ + Host: "host.header", + Header: tt.args.h, + }), "headers: %+v", tt.args.h) + }) + } +} From 3bbcc3434ab58d11bb24a07949f54eedcf3cea5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Tue, 17 Oct 2023 18:19:51 +0300 Subject: [PATCH 13/48] chore(deps): upgrade to oidc v3 (#6737) This pr upgrades oidc to v3 . Function signature changes have been migrated as well. Specifically there are more client calls that take a context now. Where feasable a context is added to those calls. Where a context is not (easily) available context.TODO() is used as a reminder for when it does. Related to #6619 --- cmd/start/start.go | 2 +- go.mod | 30 +++--- go.sum | 53 +++++------ internal/api/authz/token.go | 6 +- internal/api/grpc/management/information.go | 2 +- internal/api/grpc/management/user.go | 2 +- internal/api/grpc/oidc/v2/oidc.go | 2 +- .../api/grpc/oidc/v2/oidc_integration_test.go | 12 +-- internal/api/grpc/oidc/v2/server.go | 2 +- .../api/grpc/system/instance_converter.go | 2 +- internal/api/oidc/auth_request.go | 4 +- internal/api/oidc/auth_request_converter.go | 4 +- .../api/oidc/auth_request_converter_v2.go | 2 +- .../api/oidc/auth_request_integration_test.go | 91 ++++++++----------- internal/api/oidc/client.go | 6 +- internal/api/oidc/client_converter.go | 4 +- internal/api/oidc/client_credentials.go | 4 +- internal/api/oidc/client_integration_test.go | 14 +-- internal/api/oidc/device_auth.go | 4 +- internal/api/oidc/jwt-profile.go | 4 +- internal/api/oidc/key.go | 4 +- internal/api/oidc/oidc_integration_test.go | 12 +-- internal/api/oidc/op.go | 4 +- internal/api/saml/certificate.go | 2 +- internal/api/ui/console/console.go | 2 +- internal/api/ui/login/custom_action.go | 2 +- .../api/ui/login/external_provider_handler.go | 4 +- internal/api/ui/login/jwt_handler.go | 2 +- .../eventstore/token_verifier.go | 8 +- internal/command/idp_intent.go | 2 +- internal/command/idp_intent_test.go | 2 +- internal/command/idp_model.go | 2 +- internal/command/instance_idp_test.go | 2 +- internal/command/org_idp_test.go | 2 +- internal/command/org_test.go | 2 +- .../command/user_human_refresh_token_test.go | 2 +- internal/idp/providers/apple/apple.go | 6 +- internal/idp/providers/apple/session.go | 2 +- internal/idp/providers/apple/session_test.go | 2 +- internal/idp/providers/azuread/azuread.go | 2 +- .../idp/providers/azuread/azuread_test.go | 4 +- internal/idp/providers/azuread/session.go | 4 +- .../idp/providers/azuread/session_test.go | 2 +- internal/idp/providers/github/session_test.go | 2 +- internal/idp/providers/gitlab/gitlab.go | 2 +- internal/idp/providers/gitlab/session_test.go | 4 +- internal/idp/providers/google/google.go | 2 +- internal/idp/providers/google/session_test.go | 4 +- internal/idp/providers/jwt/session.go | 4 +- internal/idp/providers/jwt/session_test.go | 4 +- internal/idp/providers/oauth/oauth2.go | 4 +- internal/idp/providers/oauth/oauth2_test.go | 2 +- internal/idp/providers/oauth/session.go | 6 +- internal/idp/providers/oauth/session_test.go | 2 +- internal/idp/providers/oidc/oidc.go | 6 +- internal/idp/providers/oidc/oidc_test.go | 4 +- internal/idp/providers/oidc/session.go | 6 +- internal/idp/providers/oidc/session_test.go | 6 +- internal/integration/client.go | 2 +- internal/integration/integration.go | 4 +- internal/integration/oidc.go | 24 ++--- 61 files changed, 198 insertions(+), 216 deletions(-) diff --git a/cmd/start/start.go b/cmd/start/start.go index 6a017af0db..7e49b0a390 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/saml/pkg/provider" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" diff --git a/go.mod b/go.mod index f4e513fbb1..875f6bb0e8 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/drone/envsubst v1.0.3 github.com/envoyproxy/protoc-gen-validate v1.0.2 github.com/fatih/color v1.15.0 + github.com/go-jose/go-jose/v3 v3.0.0 github.com/go-ldap/ldap/v3 v3.4.5 github.com/go-webauthn/webauthn v0.8.6 github.com/golang/mock v1.6.0 @@ -48,11 +49,12 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/gamut v0.3.1 github.com/muhlemmer/gu v0.3.1 + github.com/muhlemmer/httpforwarded v0.1.0 github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/rakyll/statik v0.1.7 - github.com/rs/cors v1.10.0 + github.com/rs/cors v1.10.1 github.com/sony/sonyflake v1.2.0 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 @@ -60,36 +62,35 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.4.0 - github.com/zitadel/oidc/v2 v2.11.0 + github.com/zitadel/oidc/v3 v3.0.2 github.com/zitadel/passwap v0.4.0 github.com/zitadel/saml v0.1.2 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.43.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.43.0 - go.opentelemetry.io/otel v1.17.0 + go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 go.opentelemetry.io/otel/exporters/prometheus v0.40.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0 - go.opentelemetry.io/otel/metric v1.17.0 + go.opentelemetry.io/otel/metric v1.19.0 go.opentelemetry.io/otel/sdk v1.17.0 go.opentelemetry.io/otel/sdk/metric v0.40.0 - go.opentelemetry.io/otel/trace v1.17.0 - golang.org/x/crypto v0.13.0 - golang.org/x/net v0.15.0 - golang.org/x/oauth2 v0.12.0 + go.opentelemetry.io/otel/trace v1.19.0 + golang.org/x/crypto v0.14.0 + golang.org/x/net v0.17.0 + golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.3.0 golang.org/x/text v0.13.0 google.golang.org/api v0.138.0 google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d google.golang.org/grpc v1.57.0 google.golang.org/protobuf v1.31.0 - gopkg.in/square/go-jose.v2 v2.6.0 sigs.k8s.io/yaml v1.3.0 ) require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect github.com/crewjam/httperr v0.2.0 // indirect - github.com/dmarkham/enumer v1.5.8 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect @@ -105,14 +106,11 @@ require ( github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/muhlemmer/httpforwarded v0.1.0 // indirect - github.com/pascaldekloe/name v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/smartystreets/assertions v1.0.0 // indirect github.com/zenazn/goji v1.0.1 // indirect + github.com/zitadel/schema v1.3.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect ) @@ -154,7 +152,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/uuid v1.3.1 // indirect + github.com/google/uuid v1.3.1 github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect @@ -203,7 +201,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/sys v0.12.0 + golang.org/x/sys v0.13.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 9e7ae585c7..018d31f9ef 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,6 @@ github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwu github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= -github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog= @@ -267,6 +265,8 @@ github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= @@ -275,6 +275,8 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -720,8 +722,6 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= -github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= @@ -782,8 +782,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8= -github.com/rs/cors v1.10.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -883,12 +883,14 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA= github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc= -github.com/zitadel/oidc/v2 v2.11.0 h1:Am4/yQr4iiM5bznRgF3FOp+wLdKx2gzSU73uyI9vvBE= -github.com/zitadel/oidc/v2 v2.11.0/go.mod h1:enFSVBQI6aE0TEB1ntjXs9r6O6DEosxX4uhEBLBVD8o= +github.com/zitadel/oidc/v3 v3.0.2 h1:fw0EAjx8lIlDMJ54hDz2fWIhpW/Y13tW5gd1qWGqbr4= +github.com/zitadel/oidc/v3 v3.0.2/go.mod h1:ne9V9FHug4iUZDV42JirWVLHcbmwaxY8LnkcfekHgRg= github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0= github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g= github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo= github.com/zitadel/saml v0.1.2/go.mod h1:M+X+3vMUulpoLofKeH/W1/qjQQ3owitc2GuGDu3oYpM= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -905,8 +907,8 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.43.0/go.mod h1:1WpsUwjQrUJSNugfMlPn0rPRJ9Do7wwBgTBPK7MLiS4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.43.0 h1:HKORGpiOY0R0nAPtKx/ub8/7XoHhRooP8yNRkuPfelI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.43.0/go.mod h1:e+y1M74SYXo/FcIx3UATwth2+5dDkM8dBi7eXg1tbw8= -go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= -go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 h1:U5GYackKpVKlPrd/5gKMlrTlP2dCESAAFU682VCpieY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0/go.mod h1:aFsJfCEnLzEu9vRRAcUiB/cpRTbVsNdF3OHSPpdjxZQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 h1:iGeIsSYwpYSvh5UGzWrJfTDJvPjrXtxl3GUppj6IXQU= @@ -915,14 +917,14 @@ go.opentelemetry.io/otel/exporters/prometheus v0.40.0 h1:9h6lCssr1j5aYVvWT6oc+ER go.opentelemetry.io/otel/exporters/prometheus v0.40.0/go.mod h1:5USWZ0ovyQB5CIM3IO3bGRSoDPMXiT3t+15gu8Zo9HQ= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0 h1:Ut6hgtYcASHwCzRHkXEtSsM251cXJPW+Z9DyLwEn6iI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0/go.mod h1:TYeE+8d5CjrgBa0ZuRaDeMpIC1xZ7atg4g+nInjuSjc= -go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= -go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk/metric v0.40.0 h1:qOM29YaGcxipWjL5FzpyZDpCYrDREvX0mVlmXdOjCHU= go.opentelemetry.io/otel/sdk/metric v0.40.0/go.mod h1:dWxHtdzdJvg+ciJUKLTKwrMe5P6Dv3FyDbh8UkfgkVs= -go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= -go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= @@ -952,6 +954,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -965,8 +968,8 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1007,8 +1010,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1059,8 +1060,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1070,8 +1071,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1154,8 +1155,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1242,8 +1243,6 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1384,8 +1383,6 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/internal/api/authz/token.go b/internal/api/authz/token.go index 615543093b..187a97c90d 100644 --- a/internal/api/authz/token.go +++ b/internal/api/authz/token.go @@ -10,8 +10,8 @@ import ( "sync" "time" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/go-jose/go-jose/v3" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/crypto" caos_errs "github.com/zitadel/zitadel/internal/errors" @@ -28,7 +28,7 @@ type TokenVerifier struct { authZRepo authZRepo clients sync.Map authMethods MethodMapping - systemJWTProfile op.JWTProfileVerifier + systemJWTProfile *op.JWTProfileVerifier } type MembershipsResolver interface { diff --git a/internal/api/grpc/management/information.go b/internal/api/grpc/management/information.go index 29ce6ac01c..d18e115bfa 100644 --- a/internal/api/grpc/management/information.go +++ b/internal/api/grpc/management/information.go @@ -3,7 +3,7 @@ package management import ( "context" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 2cc4fe21de..de7f57b25a 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -4,7 +4,7 @@ import ( "context" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/durationpb" diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index d35a5dcff0..465303b98a 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -4,7 +4,7 @@ import ( "context" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/internal/api/grpc/oidc/v2/oidc_integration_test.go b/internal/api/grpc/oidc/v2/oidc_integration_test.go index b64b71325d..db11f9022b 100644 --- a/internal/api/grpc/oidc/v2/oidc_integration_test.go +++ b/internal/api/grpc/oidc/v2/oidc_integration_test.go @@ -54,7 +54,7 @@ func TestServer_GetAuthRequest(t *testing.T) { require.NoError(t, err) client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) require.NoError(t, err) - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) now := time.Now() @@ -134,7 +134,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "session not found", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -151,7 +151,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "session token invalid", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -168,7 +168,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "fail callback", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -192,7 +192,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "code callback", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -217,7 +217,7 @@ func TestServer_CreateCallback(t *testing.T) { AuthRequestId: func() string { client, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit) require.NoError(t, err) - authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit) + authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit) require.NoError(t, err) return authRequestID }(), diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go index a7587dfdcf..823594fbd0 100644 --- a/internal/api/grpc/oidc/v2/server.go +++ b/internal/api/grpc/oidc/v2/server.go @@ -1,7 +1,7 @@ package oidc import ( - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 4eea0a18f7..551079aec5 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -3,7 +3,7 @@ package system import ( "strings" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/grpc/authn" diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 4ba077d043..56c0902b11 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -7,8 +7,8 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 5999f21e5e..8fbd18530d 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/api/oidc/auth_request_converter_v2.go b/internal/api/oidc/auth_request_converter_v2.go index 9f4c0d0a1f..3a35b01578 100644 --- a/internal/api/oidc/auth_request_converter_v2.go +++ b/internal/api/oidc/auth_request_converter_v2.go @@ -3,7 +3,7 @@ package oidc import ( "time" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/command" ) diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go index 532814bee1..5b013fe864 100644 --- a/internal/api/oidc/auth_request_integration_test.go +++ b/internal/api/oidc/auth_request_integration_test.go @@ -11,8 +11,8 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" http_utils "github.com/zitadel/zitadel/internal/api/http" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" @@ -103,7 +103,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) { assert.Equal(t, "state", values.Get("state")) // check id_token / claims - provider, err := Tester.CreateRelyingParty(clientID, redirectURIImplicit) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURIImplicit) require.NoError(t, err) claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier()) require.NoError(t, err) @@ -147,7 +147,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) { func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -177,13 +177,13 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) { assertIDTokenClaims(t, newTokens.IDTokenClaims, armPasskey, startTime, changeTime) // refresh with an old refresh_token must fail - _, err = rp.RefreshAccessToken(provider, tokens.RefreshToken, "", "") + _, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "") require.Error(t, err) } func TestOPStorage_RevokeToken_access_token(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -206,11 +206,11 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke access token - err = rp.RevokeToken(provider, tokens.AccessToken, "access_token") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "access_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh grant must still work @@ -218,15 +218,15 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) { require.NoError(t, err) // revocation with the same access token must not fail (with or without hint) - err = rp.RevokeToken(provider, tokens.AccessToken, "access_token") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "access_token") require.NoError(t, err) - err = rp.RevokeToken(provider, tokens.AccessToken, "") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "") require.NoError(t, err) } func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -249,11 +249,11 @@ func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke access token - err = rp.RevokeToken(provider, tokens.AccessToken, "refresh_token") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "refresh_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh grant must still work @@ -263,7 +263,7 @@ func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -286,11 +286,11 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke refresh token -> invalidates also access token - err = rp.RevokeToken(provider, tokens.RefreshToken, "refresh_token") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "refresh_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh must fail @@ -298,15 +298,15 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { require.Error(t, err) // revocation with the same refresh token must not fail (with or without hint) - err = rp.RevokeToken(provider, tokens.RefreshToken, "refresh_token") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "refresh_token") require.NoError(t, err) - err = rp.RevokeToken(provider, tokens.RefreshToken, "") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "") require.NoError(t, err) } func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -329,11 +329,11 @@ func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing. assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke refresh token even with a wrong hint - err = rp.RevokeToken(provider, tokens.RefreshToken, "access_token") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "access_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh must fail @@ -365,15 +365,15 @@ func TestOPStorage_RevokeToken_invalid_client(t *testing.T) { // simulate second client (not part of the audience) trying to revoke the token otherClientID := createClient(t) - provider, err := Tester.CreateRelyingParty(otherClientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, otherClientID, redirectURI) require.NoError(t, err) - err = rp.RevokeToken(provider, tokens.AccessToken, "") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "") require.Error(t, err) } func TestOPStorage_TerminateSession(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -396,21 +396,21 @@ func TestOPStorage_TerminateSession(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // userinfo must not fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) - postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) } func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -433,28 +433,28 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // userinfo must not fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) - postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) refreshedTokens, err := refreshTokens(t, clientID, tokens.RefreshToken) require.NoError(t, err) // userinfo must not fail - _, err = rp.Userinfo(refreshedTokens.AccessToken, refreshedTokens.TokenType, refreshedTokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, refreshedTokens.AccessToken, refreshedTokens.TokenType, refreshedTokens.IDTokenClaims.Subject, provider) require.NoError(t, err) } func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -476,12 +476,12 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { assertTokens(t, tokens, false) assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) - postLogoutRedirect, err := rp.EndSession(provider, "", logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, http_utils.BuildOrigin(Tester.Host(), Tester.Config.ExternalSecure)+Tester.Config.OIDC.DefaultLogoutURLV2+logoutRedirectURI+"?state=state", postLogoutRedirect.String()) // userinfo must not fail until login UI terminated session - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) // simulate termination by login UI @@ -492,12 +492,12 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) } func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDTokenClaims], error) { - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) codeVerifier := "codeVerifier" @@ -505,23 +505,10 @@ func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDT } func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) { - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) - tokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "") - if err != nil { - return nil, err - } - idToken, _ := tokens.Extra("id_token").(string) - claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), tokens.AccessToken, idToken, provider.IDTokenVerifier()) - if err != nil { - return nil, err - } - return &oidc.Tokens[*oidc.IDTokenClaims]{ - Token: tokens, - IDToken: idToken, - IDTokenClaims: claims, - }, nil + return rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, refreshToken, "", "") } func assertCodeResponse(t *testing.T, callback string) string { diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 4bfbb6449a..7c56288171 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -9,10 +9,10 @@ import ( "time" "github.com/dop251/goja" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/actions/object" diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index 5f3f25f759..ec208db27c 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -4,8 +4,8 @@ import ( "strings" "time" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/api/oidc/client_credentials.go b/internal/api/oidc/client_credentials.go index fda1f6c94a..3c2f272ead 100644 --- a/internal/api/oidc/client_credentials.go +++ b/internal/api/oidc/client_credentials.go @@ -3,8 +3,8 @@ package oidc import ( "time" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) type clientCredentialsRequest struct { diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go index 11121a8fae..8a7f58a441 100644 --- a/internal/api/oidc/client_integration_test.go +++ b/internal/api/oidc/client_integration_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/client/rs" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" @@ -41,9 +41,9 @@ func TestOPStorage_SetUserinfoFromToken(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // test actual userinfo - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) - userinfo, err := rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) assertUserinfo(t, userinfo) } @@ -62,7 +62,7 @@ func TestOPStorage_SetIntrospectionFromToken(t *testing.T) { ExpirationDate: nil, }) require.NoError(t, err) - resourceServer, err := Tester.CreateResourceServer(keyResp.GetKeyDetails()) + resourceServer, err := Tester.CreateResourceServer(CTX, keyResp.GetKeyDetails()) require.NoError(t, err) scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess} @@ -87,7 +87,7 @@ func TestOPStorage_SetIntrospectionFromToken(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // test actual introspection - introspection, err := rs.Introspect(context.Background(), resourceServer, tokens.AccessToken) + introspection, err := rs.Introspect[*oidc.IntrospectionResponse](context.Background(), resourceServer, tokens.AccessToken) require.NoError(t, err) assertIntrospection(t, introspection, Tester.OIDCIssuer(), app.GetClientId(), diff --git a/internal/api/oidc/device_auth.go b/internal/api/oidc/device_auth.go index 7eee06096a..80298e8afd 100644 --- a/internal/api/oidc/device_auth.go +++ b/internal/api/oidc/device_auth.go @@ -5,8 +5,8 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/api/oidc/jwt-profile.go b/internal/api/oidc/jwt-profile.go index 47805783c9..e2592a5734 100644 --- a/internal/api/oidc/jwt-profile.go +++ b/internal/api/oidc/jwt-profile.go @@ -3,8 +3,8 @@ package oidc import ( "context" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 237af7db49..b6252a565e 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -5,9 +5,9 @@ import ( "fmt" "time" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go index 1a1516040d..7738e3d615 100644 --- a/internal/api/oidc/oidc_integration_test.go +++ b/internal/api/oidc/oidc_integration_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/grpc/metadata" "github.com/zitadel/zitadel/internal/domain" @@ -216,7 +216,7 @@ func Test_ZITADEL_API_inactive_access_token(t *testing.T) { func Test_ZITADEL_API_terminated_session(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -245,7 +245,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) { require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId()) // refresh token - postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) @@ -271,13 +271,13 @@ func createImplicitClient(t testing.TB) string { } func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string { - redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) + redURL, err := Tester.CreateOIDCAuthRequest(CTX, clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) require.NoError(t, err) return redURL } func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string { - redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) + redURL, err := Tester.CreateOIDCAuthRequestImplicit(CTX, clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) require.NoError(t, err) return redURL } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index da96302e60..c165b117b9 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -7,8 +7,8 @@ import ( "time" "github.com/rakyll/statik/fs" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/assets" diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index 9e85e50982..972b3ac169 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -5,9 +5,9 @@ import ( "fmt" "time" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" "github.com/zitadel/saml/pkg/provider/key" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 7e00605b7b..6f632136f0 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -15,7 +15,7 @@ import ( "github.com/gorilla/mux" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/cmd/build" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 22b3f72ccf..f7287dd1d2 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -7,7 +7,7 @@ import ( "github.com/dop251/goja" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/actions" diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index c9cc94203f..d9c8c04fc0 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -7,8 +7,8 @@ import ( "github.com/crewjam/saml/samlsp" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/api/ui/login/jwt_handler.go b/internal/api/ui/login/jwt_handler.go index 51b0795843..aa7a4466dc 100644 --- a/internal/api/ui/login/jwt_handler.go +++ b/internal/api/ui/login/jwt_handler.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" http_util "github.com/zitadel/zitadel/internal/api/http" diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index f8ca3bf76d..215338a885 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -7,10 +7,10 @@ import ( "strings" "time" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" @@ -275,7 +275,7 @@ func (repo *TokenVerifierRepo) getTokenIDAndSubject(ctx context.Context, accessT return splitToken[0], splitToken[1], true } -func (repo *TokenVerifierRepo) jwtTokenVerifier(ctx context.Context) op.AccessTokenVerifier { +func (repo *TokenVerifierRepo) jwtTokenVerifier(ctx context.Context) *op.AccessTokenVerifier { keySet := &openIDKeySet{repo.Query} issuer := http_util.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), repo.ExternalSecure) return op.NewAccessTokenVerifier(issuer, keySet) diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 265293a3a8..edf28ef460 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -9,7 +9,7 @@ import ( "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index aeffe72eb5..27753bffbc 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -9,7 +9,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index 2736087de9..fc0665b57e 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -7,7 +7,7 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rp" "golang.org/x/oauth2" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index a0be4798c6..bf9456bf10 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index f85fb1c216..7740c01865 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 8696b3fb26..346f9f11a4 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -7,7 +7,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/command/user_human_refresh_token_test.go b/internal/command/user_human_refresh_token_test.go index 760604c66d..10f6b0ce5e 100644 --- a/internal/command/user_human_refresh_token_test.go +++ b/internal/command/user_human_refresh_token_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/idp/providers/apple/apple.go b/internal/idp/providers/apple/apple.go index 9463cb61bf..9664e006c8 100644 --- a/internal/idp/providers/apple/apple.go +++ b/internal/idp/providers/apple/apple.go @@ -5,9 +5,9 @@ import ( "encoding/pem" "time" - "github.com/zitadel/oidc/v2/pkg/crypto" - openid "github.com/zitadel/oidc/v2/pkg/oidc" - "gopkg.in/square/go-jose.v2" + "github.com/go-jose/go-jose/v3" + "github.com/zitadel/oidc/v3/pkg/crypto" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go index ae629f9573..5e9143f050 100644 --- a/internal/idp/providers/apple/session.go +++ b/internal/idp/providers/apple/session.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/apple/session_test.go b/internal/idp/providers/apple/session_test.go index 207f219813..3c1ab59763 100644 --- a/internal/idp/providers/apple/session_test.go +++ b/internal/idp/providers/apple/session_test.go @@ -9,7 +9,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/azuread/azuread.go b/internal/idp/providers/azuread/azuread.go index 244f383e1c..46445a3977 100644 --- a/internal/idp/providers/azuread/azuread.go +++ b/internal/idp/providers/azuread/azuread.go @@ -3,7 +3,7 @@ package azuread import ( "fmt" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/azuread/azuread_test.go b/internal/idp/providers/azuread/azuread_test.go index 3febb43f95..122a70bb07 100644 --- a/internal/idp/providers/azuread/azuread_test.go +++ b/internal/idp/providers/azuread/azuread_test.go @@ -6,8 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oauth" diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index 5bc7bb84c9..698cdad198 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -3,8 +3,8 @@ package azuread import ( "net/http" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp/providers/oauth" ) diff --git a/internal/idp/providers/azuread/session_test.go b/internal/idp/providers/azuread/session_test.go index 531d909b92..f68c4cc7d7 100644 --- a/internal/idp/providers/azuread/session_test.go +++ b/internal/idp/providers/azuread/session_test.go @@ -10,7 +10,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/github/session_test.go b/internal/idp/providers/github/session_test.go index 2ad9f449fc..247ef35c68 100644 --- a/internal/idp/providers/github/session_test.go +++ b/internal/idp/providers/github/session_test.go @@ -10,7 +10,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/gitlab/gitlab.go b/internal/idp/providers/gitlab/gitlab.go index 1bb02302f3..76e9a74b40 100644 --- a/internal/idp/providers/gitlab/gitlab.go +++ b/internal/idp/providers/gitlab/gitlab.go @@ -1,7 +1,7 @@ package gitlab import ( - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/gitlab/session_test.go b/internal/idp/providers/gitlab/session_test.go index de59044326..0853f3ce3e 100644 --- a/internal/idp/providers/gitlab/session_test.go +++ b/internal/idp/providers/gitlab/session_test.go @@ -9,8 +9,8 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/google/google.go b/internal/idp/providers/google/google.go index 7036d4e101..221f2b61ae 100644 --- a/internal/idp/providers/google/google.go +++ b/internal/idp/providers/google/google.go @@ -1,7 +1,7 @@ package google import ( - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/google/session_test.go b/internal/idp/providers/google/session_test.go index d3edde0ea3..1915adf1bc 100644 --- a/internal/idp/providers/google/session_test.go +++ b/internal/idp/providers/google/session_test.go @@ -9,8 +9,8 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index be3ffc0531..54fcc039eb 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -8,8 +8,8 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/idp/providers/jwt/session_test.go b/internal/idp/providers/jwt/session_test.go index ae79ed22e5..3a8210aec8 100644 --- a/internal/idp/providers/jwt/session_test.go +++ b/internal/idp/providers/jwt/session_test.go @@ -7,14 +7,14 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3" "github.com/golang/mock/gomock" "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/idp/providers/oauth/oauth2.go b/internal/idp/providers/oauth/oauth2.go index a31e9d4c26..ad8cbbd45b 100644 --- a/internal/idp/providers/oauth/oauth2.go +++ b/internal/idp/providers/oauth/oauth2.go @@ -3,8 +3,8 @@ package oauth import ( "context" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "github.com/zitadel/zitadel/internal/idp" diff --git a/internal/idp/providers/oauth/oauth2_test.go b/internal/idp/providers/oauth/oauth2_test.go index 5fdfbc2185..814a7ac9c2 100644 --- a/internal/idp/providers/oauth/oauth2_test.go +++ b/internal/idp/providers/oauth/oauth2_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rp" "golang.org/x/oauth2" "github.com/zitadel/zitadel/internal/idp" diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index e85116afac..810fc0472c 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -5,9 +5,9 @@ import ( "errors" "net/http" - "github.com/zitadel/oidc/v2/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" ) diff --git a/internal/idp/providers/oauth/session_test.go b/internal/idp/providers/oauth/session_test.go index 9901ea11c3..ce6fa3f10f 100644 --- a/internal/idp/providers/oauth/session_test.go +++ b/internal/idp/providers/oauth/session_test.go @@ -9,7 +9,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/oidc/oidc.go b/internal/idp/providers/oidc/oidc.go index aab5255488..30d4d11abf 100644 --- a/internal/idp/providers/oidc/oidc.go +++ b/internal/idp/providers/oidc/oidc.go @@ -3,8 +3,8 @@ package oidc import ( "context" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" ) @@ -100,7 +100,7 @@ func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []stri for _, option := range options { option(provider) } - provider.RelyingParty, err = rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, setDefaultScope(scopes), provider.options...) + provider.RelyingParty, err = rp.NewRelyingPartyOIDC(context.TODO(), issuer, clientID, clientSecret, redirectURI, setDefaultScope(scopes), provider.options...) if err != nil { return nil, err } diff --git a/internal/idp/providers/oidc/oidc_test.go b/internal/idp/providers/oidc/oidc_test.go index bbe08155c8..d510bf15c2 100644 --- a/internal/idp/providers/oidc/oidc_test.go +++ b/internal/idp/providers/oidc/oidc_test.go @@ -7,8 +7,8 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" ) diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index 366e42643a..bb44fb1146 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -38,7 +38,7 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return nil, err } } - info, err := rp.Userinfo( + info, err := rp.Userinfo[*oidc.UserInfo](ctx, s.Tokens.AccessToken, s.Tokens.TokenType, s.Tokens.IDTokenClaims.GetSubject(), diff --git a/internal/idp/providers/oidc/session_test.go b/internal/idp/providers/oidc/session_test.go index afaa358042..200dac8fc4 100644 --- a/internal/idp/providers/oidc/session_test.go +++ b/internal/idp/providers/oidc/session_test.go @@ -7,14 +7,14 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3" "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/integration/client.go b/internal/integration/client.go index 71bc9805c4..98a577be14 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -9,7 +9,7 @@ import ( crewjam_saml "github.com/crewjam/saml" "github.com/stretchr/testify/require" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" "google.golang.org/grpc" diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 3e270b1d72..61d7f9d90d 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -19,8 +19,8 @@ import ( "github.com/spf13/viper" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index ad1a44de32..b6edcd3aea 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "github.com/zitadel/oidc/v2/pkg/client" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/client/rs" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/oidc" http_util "github.com/zitadel/zitadel/internal/api/http" oidc_internal "github.com/zitadel/zitadel/internal/api/oidc" @@ -83,8 +83,8 @@ func (s *Tester) CreateAPIClient(ctx context.Context, projectID string) (*manage }) } -func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...) +func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) if err != nil { return "", err } @@ -110,8 +110,8 @@ func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string return strings.TrimPrefix(loc.String(), prefixWithHost), nil } -func (s *Tester) CreateOIDCAuthRequestImplicit(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...) +func (s *Tester) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) if err != nil { return "", err } @@ -146,12 +146,12 @@ func (s *Tester) OIDCIssuer() string { return http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure) } -func (s *Tester) CreateRelyingParty(clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { +func (s *Tester) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { if len(scope) == 0 { scope = []string{oidc.ScopeOpenID} } loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}} - return rp.NewRelyingPartyOIDC(s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) + return rp.NewRelyingPartyOIDC(ctx, s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) } type loginRoundTripper struct { @@ -163,12 +163,12 @@ func (c *loginRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) return c.RoundTripper.RoundTrip(req) } -func (s *Tester) CreateResourceServer(keyFileData []byte) (rs.ResourceServer, error) { +func (s *Tester) CreateResourceServer(ctx context.Context, keyFileData []byte) (rs.ResourceServer, error) { keyFile, err := client.ConfigFromKeyFileData(keyFileData) if err != nil { return nil, err } - return rs.NewResourceServerJWTProfile(s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key)) + return rs.NewResourceServerJWTProfile(ctx, s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key)) } func GetRequest(url string, headers map[string]string) (*http.Request, error) { From fb2bd157809a1b41ba58ef0f5b9da687111f17de Mon Sep 17 00:00:00 2001 From: Silvan Date: Tue, 17 Oct 2023 17:53:00 +0200 Subject: [PATCH 14/48] ci: allow restore errors (#6740) --- .github/workflows/console.yml | 1 + .github/workflows/core-integration-test.yml | 1 + .github/workflows/core-unit-test.yml | 1 + .github/workflows/core.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/console.yml b/.github/workflows/console.yml index bc4c5f0898..8471bf5af8 100644 --- a/.github/workflows/console.yml +++ b/.github/workflows/console.yml @@ -30,6 +30,7 @@ jobs: - uses: actions/cache/restore@v3 timeout-minutes: 1 + continue-on-error: true id: cache with: key: console-${{ hashFiles('console', 'proto', '!console/dist') }} diff --git a/.github/workflows/core-integration-test.yml b/.github/workflows/core-integration-test.yml index c99987d25a..f57a79767e 100644 --- a/.github/workflows/core-integration-test.yml +++ b/.github/workflows/core-integration-test.yml @@ -56,6 +56,7 @@ jobs: uses: actions/cache/restore@v3 id: cache timeout-minutes: 1 + continue-on-error: true name: restore previous results with: key: integration-test-postgres-${{ inputs.core_cache_key }} diff --git a/.github/workflows/core-unit-test.yml b/.github/workflows/core-unit-test.yml index c831758d42..3097cfd57d 100644 --- a/.github/workflows/core-unit-test.yml +++ b/.github/workflows/core-unit-test.yml @@ -40,6 +40,7 @@ jobs: uses: actions/cache/restore@v3 id: cache timeout-minutes: 1 + continue-on-error: true name: restore previous results with: key: unit-test-${{ inputs.core_cache_key }} diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index c5d8685334..45c4bbda02 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -41,6 +41,7 @@ jobs: - uses: actions/cache/restore@v3 timeout-minutes: 1 + continue-on-error: true id: cache with: key: core-${{ hashFiles( 'go.*', 'openapi', 'cmd', 'pkg/grpc/**/*.go', 'proto', 'internal') }} From c06dc106b81525e8ec6b3a929a100e2de661b5a5 Mon Sep 17 00:00:00 2001 From: Miguel Cabrerizo <30386061+doncicuto@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:46:45 +0200 Subject: [PATCH 15/48] fix(Makefile): add -r to delete .artifacts/grpc (#6697) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6495993ede..dd07fe49c4 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ console_build: console_dependencies console_client .PHONY: clean clean: - $(RM) .artifacts/grpc + $(RM) -r .artifacts/grpc $(RM) $(gen_authopt_path) $(RM) $(gen_zitadel_path) From bd23a7a56f3267af59365f7b4586a7d549da6a5f Mon Sep 17 00:00:00 2001 From: adlerhurst Date: Thu, 19 Oct 2023 12:34:00 +0200 Subject: [PATCH 16/48] merge main into next --- .github/workflows/console.yml | 1 + .github/workflows/core-integration-test.yml | 1 + .github/workflows/core-unit-test.yml | 1 + .github/workflows/core.yml | 1 + Makefile | 2 +- build/workflow.Dockerfile | 4 +- cmd/defaults.yaml | 2 +- cmd/setup/steps.yaml | 4 +- cmd/start/start.go | 2 +- .../auth-user-mfa/auth-user-mfa.component.ts | 4 +- .../user-mfa/user-mfa.component.ts | 30 ++++ console/src/app/services/mgmt.service.ts | 16 ++ docs/docs/apis/actions/modules.md | 55 ++++++ docs/docs/legal/onboarding-support.md | 87 ++++++++++ docs/docs/legal/support-services.md | 5 +- docs/docs/support/trainings/application.md | 36 ---- docs/docs/support/trainings/introduction.md | 30 ---- docs/docs/support/trainings/project.md | 35 ---- docs/docs/support/trainings/recurring.md | 33 ---- docs/sidebars.js | 17 +- go.mod | 30 ++-- go.sum | 53 +++--- internal/actions/uuid_module.go | 83 +++++++++ internal/api/authz/token.go | 6 +- internal/api/grpc/management/information.go | 2 +- internal/api/grpc/management/user.go | 22 ++- internal/api/grpc/oidc/v2/oidc.go | 2 +- .../api/grpc/oidc/v2/oidc_integration_test.go | 12 +- internal/api/grpc/oidc/v2/server.go | 2 +- .../server/middleware/auth_interceptor.go | 40 ++++- internal/api/grpc/session/v2/session.go | 57 ++++++- .../session/v2/session_integration_test.go | 84 ++++++--- internal/api/grpc/session/v2/session_test.go | 161 +++++++++++++++++- .../api/grpc/system/instance_converter.go | 2 +- internal/api/oidc/auth_request.go | 4 +- internal/api/oidc/auth_request_converter.go | 4 +- .../api/oidc/auth_request_converter_v2.go | 2 +- .../api/oidc/auth_request_integration_test.go | 91 +++++----- internal/api/oidc/client.go | 10 +- internal/api/oidc/client_converter.go | 4 +- internal/api/oidc/client_credentials.go | 4 +- internal/api/oidc/client_integration_test.go | 14 +- internal/api/oidc/device_auth.go | 4 +- internal/api/oidc/jwt-profile.go | 4 +- internal/api/oidc/key.go | 4 +- internal/api/oidc/oidc_integration_test.go | 12 +- internal/api/oidc/op.go | 4 +- internal/api/saml/certificate.go | 2 +- internal/api/ui/console/console.go | 2 +- internal/api/ui/login/custom_action.go | 10 +- .../api/ui/login/external_provider_handler.go | 4 +- internal/api/ui/login/jwt_handler.go | 2 +- .../eventstore/token_verifier.go | 8 +- internal/command/auth_request_test.go | 44 ++++- internal/command/idp_intent.go | 2 +- internal/command/idp_intent_test.go | 2 +- internal/command/idp_model.go | 2 +- internal/command/instance_idp_test.go | 2 +- internal/command/oidc_session_test.go | 22 ++- internal/command/org_idp_test.go | 2 +- internal/command/org_test.go | 2 +- internal/command/session.go | 8 +- internal/command/session_test.go | 88 ++++++++-- .../command/user_human_refresh_token_test.go | 2 +- internal/crypto/passwap.go | 2 +- internal/crypto/passwap_test.go | 5 +- internal/domain/user_agent.go | 17 ++ internal/idp/providers/apple/apple.go | 6 +- internal/idp/providers/apple/session.go | 2 +- internal/idp/providers/apple/session_test.go | 2 +- internal/idp/providers/azuread/azuread.go | 2 +- .../idp/providers/azuread/azuread_test.go | 4 +- internal/idp/providers/azuread/session.go | 4 +- .../idp/providers/azuread/session_test.go | 2 +- internal/idp/providers/github/session_test.go | 2 +- internal/idp/providers/gitlab/gitlab.go | 2 +- internal/idp/providers/gitlab/session_test.go | 4 +- internal/idp/providers/google/google.go | 2 +- internal/idp/providers/google/session_test.go | 4 +- internal/idp/providers/jwt/session.go | 4 +- internal/idp/providers/jwt/session_test.go | 4 +- internal/idp/providers/oauth/oauth2.go | 4 +- internal/idp/providers/oauth/oauth2_test.go | 2 +- internal/idp/providers/oauth/session.go | 6 +- internal/idp/providers/oauth/session_test.go | 2 +- internal/idp/providers/oidc/oidc.go | 6 +- internal/idp/providers/oidc/oidc_test.go | 4 +- internal/idp/providers/oidc/session.go | 6 +- internal/idp/providers/oidc/session_test.go | 6 +- internal/integration/client.go | 2 +- internal/integration/integration.go | 4 +- internal/integration/oidc.go | 24 +-- .../protoc-gen-zitadel/zitadel.pb.go.tmpl | 4 +- internal/query/prepare_test.go | 12 +- internal/query/projection/session.go | 94 ++++++---- internal/query/projection/session_test.go | 38 +++-- internal/query/session.go | 33 ++++ internal/query/sessions_test.go | 107 +++++++----- internal/repository/session/session.go | 3 + .../v2beta/user_service_org.pb.zitadel.go | 12 ++ proto/zitadel/admin.proto | 12 +- proto/zitadel/management.proto | 94 +++++++++- proto/zitadel/object/v2beta/object.proto | 8 + proto/zitadel/project.proto | 2 +- proto/zitadel/session/v2beta/session.proto | 24 ++- .../session/v2beta/session_service.proto | 1 + proto/zitadel/user/v2beta/user_service.proto | 10 +- 107 files changed, 1321 insertions(+), 554 deletions(-) create mode 100644 docs/docs/legal/onboarding-support.md delete mode 100644 docs/docs/support/trainings/application.md delete mode 100644 docs/docs/support/trainings/introduction.md delete mode 100644 docs/docs/support/trainings/project.md delete mode 100644 docs/docs/support/trainings/recurring.md create mode 100644 internal/actions/uuid_module.go create mode 100644 internal/domain/user_agent.go create mode 100644 pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go diff --git a/.github/workflows/console.yml b/.github/workflows/console.yml index bc4c5f0898..8471bf5af8 100644 --- a/.github/workflows/console.yml +++ b/.github/workflows/console.yml @@ -30,6 +30,7 @@ jobs: - uses: actions/cache/restore@v3 timeout-minutes: 1 + continue-on-error: true id: cache with: key: console-${{ hashFiles('console', 'proto', '!console/dist') }} diff --git a/.github/workflows/core-integration-test.yml b/.github/workflows/core-integration-test.yml index c99987d25a..f57a79767e 100644 --- a/.github/workflows/core-integration-test.yml +++ b/.github/workflows/core-integration-test.yml @@ -56,6 +56,7 @@ jobs: uses: actions/cache/restore@v3 id: cache timeout-minutes: 1 + continue-on-error: true name: restore previous results with: key: integration-test-postgres-${{ inputs.core_cache_key }} diff --git a/.github/workflows/core-unit-test.yml b/.github/workflows/core-unit-test.yml index c831758d42..3097cfd57d 100644 --- a/.github/workflows/core-unit-test.yml +++ b/.github/workflows/core-unit-test.yml @@ -40,6 +40,7 @@ jobs: uses: actions/cache/restore@v3 id: cache timeout-minutes: 1 + continue-on-error: true name: restore previous results with: key: unit-test-${{ inputs.core_cache_key }} diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index c5d8685334..45c4bbda02 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -41,6 +41,7 @@ jobs: - uses: actions/cache/restore@v3 timeout-minutes: 1 + continue-on-error: true id: cache with: key: core-${{ hashFiles( 'go.*', 'openapi', 'cmd', 'pkg/grpc/**/*.go', 'proto', 'internal') }} diff --git a/Makefile b/Makefile index 6495993ede..dd07fe49c4 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ console_build: console_dependencies console_client .PHONY: clean clean: - $(RM) .artifacts/grpc + $(RM) -r .artifacts/grpc $(RM) $(gen_authopt_path) $(RM) $(gen_zitadel_path) diff --git a/build/workflow.Dockerfile b/build/workflow.Dockerfile index c9fb7e6c2b..db27daf91c 100644 --- a/build/workflow.Dockerfile +++ b/build/workflow.Dockerfile @@ -103,7 +103,7 @@ COPY --from=core-assets /go/src/github.com/zitadel/zitadel/internal ./internal # ####################################### # download console dependencies # ####################################### -FROM node:18-buster AS console-deps +FROM node:20-buster AS console-deps WORKDIR /zitadel/console @@ -115,7 +115,7 @@ RUN yarn install --frozen-lockfile # ####################################### # generate console client # ####################################### -FROM node:18-buster AS console-client +FROM node:20-buster AS console-client WORKDIR /zitadel/console diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 7897e12834..a681bedca2 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -767,7 +767,7 @@ DefaultInstance: PreHeader: Verify email Subject: Verify email Greeting: Hello {{.DisplayName}}, - Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email. + Text: A new email has been added. Please use the button below to verify your email. (Code {{.Code}}) If you din't add a new email, please ignore this email. ButtonText: Verify email - MessageTextType: VerifyPhone Language: en diff --git a/cmd/setup/steps.yaml b/cmd/setup/steps.yaml index 1497b7be4a..c585c535b2 100644 --- a/cmd/setup/steps.yaml +++ b/cmd/setup/steps.yaml @@ -35,8 +35,8 @@ FirstInstance: # If FirstInstance.Org.Machine.Machine is defined, a service user is created with the IAM_OWNER role, not a human user. Machine: Machine: - Username: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_USERNAME - Name: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_NAME + Username: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME + Name: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME MachineKey: # date format: 2023-01-01T00:00:00Z ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_EXPIRATIONDATE diff --git a/cmd/start/start.go b/cmd/start/start.go index dfc759e3af..744e1f80ac 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/saml/pkg/provider" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts index 6a81838c44..c4a3d64033 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts @@ -157,7 +157,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy { this.service .removeMyAuthFactorOTPEmail() .then(() => { - this.toast.showInfo('USER.TOAST.U2FREMOVED', true); + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); this.cleanupList(); this.getMFAs(); @@ -169,7 +169,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy { this.service .removeMyAuthFactorOTPSMS() .then(() => { - this.toast.showInfo('USER.TOAST.U2FREMOVED', true); + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); this.cleanupList(); this.getMFAs(); diff --git a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts index c9be008e01..29418e80fe 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-mfa/user-mfa.component.ts @@ -102,6 +102,36 @@ export class UserMfaComponent implements OnInit, OnDestroy { .catch((error) => { this.toast.showError(error); }); + } else if (factor.otpEmail) { + this.mgmtUserService + .removeHumanAuthFactorOTPEmail(this.user.id) + .then(() => { + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); + + const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpEmail); + if (index > -1) { + this.dataSource.data.splice(index, 1); + } + this.getMFAs(); + }) + .catch((error) => { + this.toast.showError(error); + }); + } else if (factor.otpSms) { + this.mgmtUserService + .removeHumanAuthFactorOTPSMS(this.user.id) + .then(() => { + this.toast.showInfo('USER.TOAST.OTPREMOVED', true); + + const index = this.dataSource.data.findIndex((mfa) => !!mfa.otpSms); + if (index > -1) { + this.dataSource.data.splice(index, 1); + } + this.getMFAs(); + }) + .catch((error) => { + this.toast.showError(error); + }); } } }); diff --git a/console/src/app/services/mgmt.service.ts b/console/src/app/services/mgmt.service.ts index c80e9e9b49..f2389a25e9 100644 --- a/console/src/app/services/mgmt.service.ts +++ b/console/src/app/services/mgmt.service.ts @@ -322,8 +322,12 @@ import { RemoveCustomLabelPolicyLogoDarkResponse, RemoveCustomLabelPolicyLogoRequest, RemoveCustomLabelPolicyLogoResponse, + RemoveHumanAuthFactorOTPEmailRequest, + RemoveHumanAuthFactorOTPEmailResponse, RemoveHumanAuthFactorOTPRequest, RemoveHumanAuthFactorOTPResponse, + RemoveHumanAuthFactorOTPSMSRequest, + RemoveHumanAuthFactorOTPSMSResponse, RemoveHumanAuthFactorU2FRequest, RemoveHumanAuthFactorU2FResponse, RemoveHumanLinkedIDPRequest, @@ -1805,6 +1809,18 @@ export class ManagementService { return this.grpcService.mgmt.removeHumanAuthFactorU2F(req, null).then((resp) => resp.toObject()); } + public removeHumanAuthFactorOTPSMS(userId: string): Promise { + const req = new RemoveHumanAuthFactorOTPSMSRequest(); + req.setUserId(userId); + return this.grpcService.mgmt.removeHumanAuthFactorOTPSMS(req, null).then((resp) => resp.toObject()); + } + + public removeHumanAuthFactorOTPEmail(userId: string): Promise { + const req = new RemoveHumanAuthFactorOTPEmailRequest(); + req.setUserId(userId); + return this.grpcService.mgmt.removeHumanAuthFactorOTPEmail(req, null).then((resp) => resp.toObject()); + } + public updateHumanProfile( userId: string, firstName?: string, diff --git a/docs/docs/apis/actions/modules.md b/docs/docs/apis/actions/modules.md index 2cc99a222e..6624ff9deb 100644 --- a/docs/docs/apis/actions/modules.md +++ b/docs/docs/apis/actions/modules.md @@ -51,3 +51,58 @@ The object has the following fields and methods: Returns the body as JSON object, or throws an error if the body is not a json object. - `text()` *string* Returns the body + +## UUID + +This module provides functionality to generate a UUID + +### Import + +```js + let uuid = require("zitadel/uuid") +``` + +### `uuid.vX()` function + +This function generates a UUID using [google/uuid](https://github.com/google/uuid). `vX` allows to define the UUID version: + +- `uuid.v1()` *string* + Generates a UUID version 1, based on date-time and MAC address +- `uuid.v3(namespace, data)` *string* + Generates a UUID version 3, based on the provided namespace using MD5 +- `uuid.v4()` *string* + Generates a UUID version 4, which is randomly generated +- `uuid.v5(namespace, data)` *string* + Generates a UUID version 5, based on the provided namespace using SHA1 + +#### Parameters + +- `namespace` *UUID*/*string* + Namespace to be used in the hashing function. Either provide one of defined [namespaces](#namespaces) or a string representing a UUID. +- `data` *[]byte*/*string* + data to be used in the hashing function. Possible types are []byte or string. + +### Namespaces + +The following predefined namespaces can be used for `uuid.v3` and `uuid.v5`: + +- `uuid.namespaceDNS` *UUID* + 6ba7b810-9dad-11d1-80b4-00c04fd430c8 +- `uuid.namespaceURL` *UUID* + 6ba7b811-9dad-11d1-80b4-00c04fd430c8 +- `uuid.namespaceOID` *UUID* + 6ba7b812-9dad-11d1-80b4-00c04fd430c8 +- `uuid.namespaceX500` *UUID* + 6ba7b814-9dad-11d1-80b4-00c04fd430c8 + +### Example +```js +let uuid = require("zitadel/uuid") +function setUUID(ctx, api) { + if (api.metadata === undefined) { + return; + } + + api.v1.user.appendMetadata('custom-id', uuid.v4()); +} +``` \ No newline at end of file diff --git a/docs/docs/legal/onboarding-support.md b/docs/docs/legal/onboarding-support.md new file mode 100644 index 0000000000..e9c76ac38e --- /dev/null +++ b/docs/docs/legal/onboarding-support.md @@ -0,0 +1,87 @@ +--- +title: Description of onboarding support services for ZITADEL +sidebar_label: Onboarding support +custom_edit_url: null +--- + +This annex of the [Framework Agreement](terms-of-service) describes the onboarding support services offered by us for our services. + +Last revised: October 12, 2023 + +Our onboarding support should help you, as a new customer, to get a better understanding on how to integrate ZITADEL into your solution, how to tackle the migration, and ensure a highly-available day-to-day operation. + +Onboarding support services can be offered to customers that enter a ZITADEL Cloud or a ZITADEL Enterprise subscription. + +If you intend to use the open source version exclusively then please join our community chat or Github. +Your questions might help other people in the community and will make our project better over time. + +Please [contact us](https://zitadel.com/contact) for a quote and to get started with onboarding support. +Below you will find topics covered and scope of the offered services. + +## Proof of value + +Within a short time-frame, f.e. 3 weeks, we can show the value of using our services and have the ability to establish the proof a of working setup for your most critical use cases. +We may offer to support you during an initial period to evaluate next steps. +Before the start of the period we may ask you to provide a description of your critical use cases and a high-level overview of your planned integration architecture. +During this period you should make sure that you have the necessary resources on your side to complete the proof of value. + +## Onboarding term + +With the onboarding support we provide the initial knowledge transfer to configure and operate ZITADEL. +During the term you will get direct access to our engineering team via [Technical Account Management](./support-services.md#technical-account-manager). +Duration is typically 3 months but this could vary depending on your requirements. + +We offer an onboarding term in combination with ZITADEL Enterprise subscriptions. + +### Topics covered + +Topics of the onboarding term may include: + +- Administration +- DevOps (Operation) +- Architecture +- Integration +- Migration +- Security Best Practices & Go-Live Checkup + +The scope will be tailored to your requirements. + +More details + +- IAM Configuration +- Walk-though all features +- Users / Manuals +- Authentication & Management APIs +- Validation of tokens +- Client integration best-practices +- Event types +- Database schemas and compute models +- Accessing database +- Observability (Logs, Errors, Metrics, Tracing) +- Operations best practices (Deployment, Backup, Networking etc.) +- Check prerequisites and architecture +- Troubleshoot installation and configuration of ZITADEL +- Troubleshoot and configuration connectivity to the database +- Functional testing of the ZITADEL instance + +
+ Out of scope +
    +
  • Performance testing
  • +
  • Setting up or maintaining backup storage
  • +
  • Running multiple ZITADEL instances on the same cluster
  • +
  • Integration into internal monitoring and alerting
  • +
  • Multi-cluster architecture deployments
  • +
  • DNS, Network and Firewall configuration
  • +
  • Customer-specific Kubernetes configuration needs
  • +
  • Non-production environments
  • +
  • Production deployment
  • +
  • Application-side coding, configuration, or tuning
  • + +
+
+ +## Continuous support + +After the onboarding phase has ended we will provide continuous support according to your subscription. +We can provide you with continued access to the technical account management in our Enterprise subscriptions. diff --git a/docs/docs/legal/support-services.md b/docs/docs/legal/support-services.md index 1c0f36b3fb..1a6f0a1555 100644 --- a/docs/docs/legal/support-services.md +++ b/docs/docs/legal/support-services.md @@ -10,7 +10,7 @@ This annex of the [Framework Agreement](terms-of-service) and the [Support Servi Support Services for products and services provided by ZITADEL is offered to customers according to the terms and conditions outlined in this document. The customer may purchase support services from ZITADEL (CAOS Ltd.) directly. -Last revised: March 15, 2023 +Last revised: October 6, 2023 ## Support Services @@ -82,7 +82,8 @@ Phone Support | +41 43 215 27 34 ZITADEL will enhance its support offering by providing eligible clients with a Technical Account Manager (TAM), who will perform the following tasks for up to the specified amount of time per week during the term of service: - Provide support and advice regarding best practices on platform, product and configuration covered by the applicable Support Services; -- Participate in review calls every other week at mutually agreed times addressing customer’s operational issues. +- Participate in review calls every other week at mutually agreed times addressing customer’s operational challenges or complex support requests; +- Walk-through of new features and customer feedback. We offer TAM services only bundled with specific subscription plans, and the option to add more TAM hours to these plans. If you require consulting for your projects, please request a quote via our [website](https://zitadel.com/contact). diff --git a/docs/docs/support/trainings/application.md b/docs/docs/support/trainings/application.md deleted file mode 100644 index cf313bb961..0000000000 --- a/docs/docs/support/trainings/application.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Application Support Trainings ---- - -## ZITADEL DevOps - -In this session your second level support and operations team will gain an understanding on how to extract relevant information for technical support questions and root cause analysis. We will also present our DevOps best practices and answer your questions. - -**Audience**: 2nd Level Support Staff, Operations -**Duration**: 0.5 day - -**Topics covered**: - -- Event types -- Database schemas and compute models -- Accessing database -- Observability (Logs, Errors, Metrics, Tracing) -- Operations best practices (Deployment, Backup, Networking etc.) -- Q&A - -## ZITADEL Administrator - -In this hands-on training your employees will get a complete overview of the system and learn how to configure and use ZITADEL. Your support staff will gain the required knowledge to provide user-support, while your solution owners gain an understanding about integration of clients. - -**Audience**: 1st / 2nd Level Support Staff, Solution Owner, QA Manager (optional) -**Duration**: 0.5 days - -**Topics covered**: - -- IAM Configuration -- Walk-though all features -- Users / Manuals -- APIs -- Validation of tokens -- Client integration best-practices -- Q&A diff --git a/docs/docs/support/trainings/introduction.md b/docs/docs/support/trainings/introduction.md deleted file mode 100644 index c597470f38..0000000000 --- a/docs/docs/support/trainings/introduction.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: ZITADEL Trainings -sidebar_label: Introduction ---- - -The following pages describe the the trainings provided by ZITADEL. These trainings are intended for onboarding and during the course of a Support Program. - -Training should be held as block-sessions with the relevant staff from your organization. - -## Onboarding Project - -You receive professional onboarding support from our engineers, who help you to setup and configure ZITADEL on your infrastructure. - -[More information](project) - -## Application Support Trainings - -With the application support trainings we provide the initial knowledge transfer to manage and support ZITADEL. The trainings are held as block-sessions with relevant staff from your organization. Prices are flat-fee, excl. expenses. - -* [ZITADEL DevOps](application#zitadel-devops) -* [ZITADEL Administrator](application#zitadel-administrator) - -## Recurring Trainings - -While you can benefit from a technical account manager during your term, these trainings are designed to onboard new staff or update staff about larger changes to the platform. Prices are flat-fee, excl. expenses. - -* [ZITADEL Support Refresher](recurring#zitadel-support-refresher) -* [ZITADEL Support Onboarding](recurring#zitadel-support-onboarding) - -In case you have any questions please [get in touch](https://zitadel.com/contact). diff --git a/docs/docs/support/trainings/project.md b/docs/docs/support/trainings/project.md deleted file mode 100644 index 35f3259b46..0000000000 --- a/docs/docs/support/trainings/project.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Onboarding Project ---- - -Effort required during depends on the complexity of your infrastructure and the overall setup. With a Multi-Zone Setup (excl. Multi-Region), support during this phase requires around 10-25h over 2 weeks. Actual effort is based on time and material. - -Scope of the project is agreed on individual basis. - -## In Scope - -- Check prerequisites and architecture -- Troubleshoot installation and configuration of ZITADEL -- Troubleshoot and configuration connectivity to the database -- Functional testing of the ZITADEL instance - -## Out of Scope - -- Running multiple ZITADEL instances on the same cluster -- Integration into internal monitoring and alerting -- Multi-cluster architecture deployments -- DNS, Network and Firewall configuration -- Customer-specific Kubernetes configuration needs -- Changes for specific environments -- Performance testing -- Production deployment -- Application-side coding, configuration, or tuning -- Changes or configuration on assets used in ZITADEL -- Setting up or maintaining backup storage - -## Prerequisites - -- Running Kubernetes with possibility to deploy to namespaces -- Inbound and outbound HTTP/2 traffic possible -- For being able to send SMS, we need a Twilio sender name, SID and an auth token -- ZITADEL also needs to connect to an email relay of your choice. We need the SMTP host, user and app key as well as the ZITADEL emails sender address and name. diff --git a/docs/docs/support/trainings/recurring.md b/docs/docs/support/trainings/recurring.md deleted file mode 100644 index 29982cf620..0000000000 --- a/docs/docs/support/trainings/recurring.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Recurring Trainings ---- - -## ZITADEL Support Refresher - -In this session you can refresh knowledge about existing and gain experience with new features of ZITADEL to keep the quality of your support high. We recommend an half day training per support staff. - -**Audience**: 1st / 2nd Level Support Staff, Solution Owner, QA Manager (optional) -**Duration**: 0.5 day / support staff - -**Topics covered**: - -* Walk-through new features -* Review of difficult support issues -* Review of customer feedback -* Q&A - -## ZITADEL Support Onboarding - -In this hands-on training new support staff will get an overview of the system and learn how to configure and use ZITADEL to provide support for users. - -**Audience**: 1st / 2nd Level Support Staff, Solution Owner, QA Manager (optional) -**Duration**: 0.5 days / support staff - -**Topics covered**: - -* Event types -* Accessing database -* Logs and Errors -* Validation of tokens -* Walk-through key features -* Q&A diff --git a/docs/sidebars.js b/docs/sidebars.js index 1c82d82852..a2ba179c80 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -425,6 +425,11 @@ module.exports = { items: [ "support/software-release-cycles-support", "support/troubleshooting", + { + type: 'link', + label: 'Support Service Descriptions', + href: '/legal/support-services', + }, { type: 'category', label: "Technical Advisory", @@ -440,17 +445,6 @@ module.exports = { }, ], }, - { - type: "category", - label: "Trainings", - collapsed: true, - items: [ - "support/trainings/introduction", - "support/trainings/application", - "support/trainings/recurring", - "support/trainings/project", - ], - }, ] }, ], @@ -697,6 +691,7 @@ module.exports = { "legal/cloud-service-description", "legal/service-level-description", "legal/support-services", + "legal/onboarding-support", ], }, { diff --git a/go.mod b/go.mod index f4e513fbb1..875f6bb0e8 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/drone/envsubst v1.0.3 github.com/envoyproxy/protoc-gen-validate v1.0.2 github.com/fatih/color v1.15.0 + github.com/go-jose/go-jose/v3 v3.0.0 github.com/go-ldap/ldap/v3 v3.4.5 github.com/go-webauthn/webauthn v0.8.6 github.com/golang/mock v1.6.0 @@ -48,11 +49,12 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/gamut v0.3.1 github.com/muhlemmer/gu v0.3.1 + github.com/muhlemmer/httpforwarded v0.1.0 github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 github.com/rakyll/statik v0.1.7 - github.com/rs/cors v1.10.0 + github.com/rs/cors v1.10.1 github.com/sony/sonyflake v1.2.0 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 @@ -60,36 +62,35 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.4.0 - github.com/zitadel/oidc/v2 v2.11.0 + github.com/zitadel/oidc/v3 v3.0.2 github.com/zitadel/passwap v0.4.0 github.com/zitadel/saml v0.1.2 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.43.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.43.0 - go.opentelemetry.io/otel v1.17.0 + go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 go.opentelemetry.io/otel/exporters/prometheus v0.40.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0 - go.opentelemetry.io/otel/metric v1.17.0 + go.opentelemetry.io/otel/metric v1.19.0 go.opentelemetry.io/otel/sdk v1.17.0 go.opentelemetry.io/otel/sdk/metric v0.40.0 - go.opentelemetry.io/otel/trace v1.17.0 - golang.org/x/crypto v0.13.0 - golang.org/x/net v0.15.0 - golang.org/x/oauth2 v0.12.0 + go.opentelemetry.io/otel/trace v1.19.0 + golang.org/x/crypto v0.14.0 + golang.org/x/net v0.17.0 + golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.3.0 golang.org/x/text v0.13.0 google.golang.org/api v0.138.0 google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d google.golang.org/grpc v1.57.0 google.golang.org/protobuf v1.31.0 - gopkg.in/square/go-jose.v2 v2.6.0 sigs.k8s.io/yaml v1.3.0 ) require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.43.1 // indirect github.com/crewjam/httperr v0.2.0 // indirect - github.com/dmarkham/enumer v1.5.8 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect @@ -105,14 +106,11 @@ require ( github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/muhlemmer/httpforwarded v0.1.0 // indirect - github.com/pascaldekloe/name v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/smartystreets/assertions v1.0.0 // indirect github.com/zenazn/goji v1.0.1 // indirect + github.com/zitadel/schema v1.3.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect ) @@ -154,7 +152,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/uuid v1.3.1 // indirect + github.com/google/uuid v1.3.1 github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect @@ -203,7 +201,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/sys v0.12.0 + golang.org/x/sys v0.13.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 9e7ae585c7..018d31f9ef 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,6 @@ github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwu github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dmarkham/enumer v1.5.8 h1:fIF11F9l5jyD++YYvxcSH5WgHfeaSGPaN/T4kOQ4qEM= -github.com/dmarkham/enumer v1.5.8/go.mod h1:d10o8R3t/gROm2p3BXqTkMt2+HMuxEmWCXzorAruYak= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog= @@ -267,6 +265,8 @@ github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= @@ -275,6 +275,8 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -720,8 +722,6 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U= -github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= @@ -782,8 +782,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8= -github.com/rs/cors v1.10.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -883,12 +883,14 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA= github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc= -github.com/zitadel/oidc/v2 v2.11.0 h1:Am4/yQr4iiM5bznRgF3FOp+wLdKx2gzSU73uyI9vvBE= -github.com/zitadel/oidc/v2 v2.11.0/go.mod h1:enFSVBQI6aE0TEB1ntjXs9r6O6DEosxX4uhEBLBVD8o= +github.com/zitadel/oidc/v3 v3.0.2 h1:fw0EAjx8lIlDMJ54hDz2fWIhpW/Y13tW5gd1qWGqbr4= +github.com/zitadel/oidc/v3 v3.0.2/go.mod h1:ne9V9FHug4iUZDV42JirWVLHcbmwaxY8LnkcfekHgRg= github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0= github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g= github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo= github.com/zitadel/saml v0.1.2/go.mod h1:M+X+3vMUulpoLofKeH/W1/qjQQ3owitc2GuGDu3oYpM= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -905,8 +907,8 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.43.0/go.mod h1:1WpsUwjQrUJSNugfMlPn0rPRJ9Do7wwBgTBPK7MLiS4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.43.0 h1:HKORGpiOY0R0nAPtKx/ub8/7XoHhRooP8yNRkuPfelI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.43.0/go.mod h1:e+y1M74SYXo/FcIx3UATwth2+5dDkM8dBi7eXg1tbw8= -go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= -go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0 h1:U5GYackKpVKlPrd/5gKMlrTlP2dCESAAFU682VCpieY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.17.0/go.mod h1:aFsJfCEnLzEu9vRRAcUiB/cpRTbVsNdF3OHSPpdjxZQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.17.0 h1:iGeIsSYwpYSvh5UGzWrJfTDJvPjrXtxl3GUppj6IXQU= @@ -915,14 +917,14 @@ go.opentelemetry.io/otel/exporters/prometheus v0.40.0 h1:9h6lCssr1j5aYVvWT6oc+ER go.opentelemetry.io/otel/exporters/prometheus v0.40.0/go.mod h1:5USWZ0ovyQB5CIM3IO3bGRSoDPMXiT3t+15gu8Zo9HQ= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0 h1:Ut6hgtYcASHwCzRHkXEtSsM251cXJPW+Z9DyLwEn6iI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.17.0/go.mod h1:TYeE+8d5CjrgBa0ZuRaDeMpIC1xZ7atg4g+nInjuSjc= -go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= -go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk/metric v0.40.0 h1:qOM29YaGcxipWjL5FzpyZDpCYrDREvX0mVlmXdOjCHU= go.opentelemetry.io/otel/sdk/metric v0.40.0/go.mod h1:dWxHtdzdJvg+ciJUKLTKwrMe5P6Dv3FyDbh8UkfgkVs= -go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= -go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= @@ -952,6 +954,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -965,8 +968,8 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1007,8 +1010,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1059,8 +1060,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1070,8 +1071,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1154,8 +1155,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1242,8 +1243,6 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1384,8 +1383,6 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/internal/actions/uuid_module.go b/internal/actions/uuid_module.go new file mode 100644 index 0000000000..15d2992127 --- /dev/null +++ b/internal/actions/uuid_module.go @@ -0,0 +1,83 @@ +package actions + +import ( + "context" + + "github.com/dop251/goja" + "github.com/google/uuid" + "github.com/zitadel/logging" +) + +func WithUUID(ctx context.Context) Option { + return func(c *runConfig) { + c.modules["zitadel/uuid"] = func(runtime *goja.Runtime, module *goja.Object) { + requireUUID(ctx, runtime, module) + } + } +} + +func requireUUID(_ context.Context, runtime *goja.Runtime, module *goja.Object) { + o := module.Get("exports").(*goja.Object) + logging.OnError(o.Set("v1", inRuntime(uuid.NewUUID, runtime))).Warn("unable to set module") + logging.OnError(o.Set("v3", inRuntimeHash(uuid.NewMD5, runtime))).Warn("unable to set module") + logging.OnError(o.Set("v4", inRuntime(uuid.NewRandom, runtime))).Warn("unable to set module") + logging.OnError(o.Set("v5", inRuntimeHash(uuid.NewSHA1, runtime))).Warn("unable to set module") + logging.OnError(o.Set("namespaceDNS", uuid.NameSpaceDNS)).Warn("unable to set namespace") + logging.OnError(o.Set("namespaceURL", uuid.NameSpaceURL)).Warn("unable to set namespace") + logging.OnError(o.Set("namespaceOID", uuid.NameSpaceOID)).Warn("unable to set namespace") + logging.OnError(o.Set("namespaceX500", uuid.NameSpaceX500)).Warn("unable to set namespace") +} + +func inRuntime(function func() (uuid.UUID, error), runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value { + return func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) != 0 { + panic("invalid arg count") + } + + uuid, err := function() + if err != nil { + logging.WithError(err) + panic(err) + } + + return runtime.ToValue(uuid.String()) + } +} + +func inRuntimeHash(function func(uuid.UUID, []byte) uuid.UUID, runtime *goja.Runtime) func(call goja.FunctionCall) goja.Value { + return func(call goja.FunctionCall) goja.Value { + if len(call.Arguments) != 2 { + logging.WithFields("count", len(call.Arguments)).Debug("other than 2 args provided") + panic("invalid arg count") + } + + var err error + var namespace uuid.UUID + switch n := call.Arguments[0].Export().(type) { + case string: + namespace, err = uuid.Parse(n) + if err != nil { + logging.WithError(err).Debug("namespace failed parsing as UUID") + panic(err) + } + case uuid.UUID: + namespace = n + default: + logging.WithError(err).Debug("invalid type for namespace") + panic(err) + } + + var data []byte + switch d := call.Arguments[1].Export().(type) { + case string: + data = []byte(d) + case []byte: + data = d + default: + logging.WithError(err).Debug("invalid type for data") + panic(err) + } + + return runtime.ToValue(function(namespace, data).String()) + } +} diff --git a/internal/api/authz/token.go b/internal/api/authz/token.go index afb4af4bf3..e5b34af9a2 100644 --- a/internal/api/authz/token.go +++ b/internal/api/authz/token.go @@ -10,8 +10,8 @@ import ( "sync" "time" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/go-jose/go-jose/v3" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/crypto" caos_errs "github.com/zitadel/zitadel/internal/errors" @@ -28,7 +28,7 @@ type TokenVerifier struct { authZRepo authZRepo clients sync.Map authMethods MethodMapping - systemJWTProfile op.JWTProfileVerifier + systemJWTProfile *op.JWTProfileVerifier } type MembershipsResolver interface { diff --git a/internal/api/grpc/management/information.go b/internal/api/grpc/management/information.go index 29ce6ac01c..d18e115bfa 100644 --- a/internal/api/grpc/management/information.go +++ b/internal/api/grpc/management/information.go @@ -3,7 +3,7 @@ package management import ( "context" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http" diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index d7a9c56dca..c61832169a 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -4,7 +4,7 @@ import ( "context" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/durationpb" @@ -647,6 +647,26 @@ func (s *Server) RemoveHumanAuthFactorU2F(ctx context.Context, req *mgmt_pb.Remo }, nil } +func (s *Server) RemoveHumanAuthFactorOTPSMS(ctx context.Context, req *mgmt_pb.RemoveHumanAuthFactorOTPSMSRequest) (*mgmt_pb.RemoveHumanAuthFactorOTPSMSResponse, error) { + objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.RemoveHumanAuthFactorOTPSMSResponse{ + Details: obj_grpc.DomainToChangeDetailsPb(objectDetails), + }, nil +} + +func (s *Server) RemoveHumanAuthFactorOTPEmail(ctx context.Context, req *mgmt_pb.RemoveHumanAuthFactorOTPEmailRequest) (*mgmt_pb.RemoveHumanAuthFactorOTPEmailResponse, error) { + objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + return &mgmt_pb.RemoveHumanAuthFactorOTPEmailResponse{ + Details: obj_grpc.DomainToChangeDetailsPb(objectDetails), + }, nil +} + func (s *Server) ListHumanPasswordless(ctx context.Context, req *mgmt_pb.ListHumanPasswordlessRequest) (*mgmt_pb.ListHumanPasswordlessResponse, error) { query := new(query.UserAuthMethodSearchQueries) err := query.AppendUserIDQuery(req.UserId) diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index d35a5dcff0..465303b98a 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -4,7 +4,7 @@ import ( "context" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/internal/api/grpc/oidc/v2/oidc_integration_test.go b/internal/api/grpc/oidc/v2/oidc_integration_test.go index b64b71325d..db11f9022b 100644 --- a/internal/api/grpc/oidc/v2/oidc_integration_test.go +++ b/internal/api/grpc/oidc/v2/oidc_integration_test.go @@ -54,7 +54,7 @@ func TestServer_GetAuthRequest(t *testing.T) { require.NoError(t, err) client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) require.NoError(t, err) - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) now := time.Now() @@ -134,7 +134,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "session not found", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -151,7 +151,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "session token invalid", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -168,7 +168,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "fail callback", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -192,7 +192,7 @@ func TestServer_CreateCallback(t *testing.T) { name: "code callback", req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) + authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI) require.NoError(t, err) return authRequestID }(), @@ -217,7 +217,7 @@ func TestServer_CreateCallback(t *testing.T) { AuthRequestId: func() string { client, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit) require.NoError(t, err) - authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit) + authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit) require.NoError(t, err) return authRequestID }(), diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go index a7587dfdcf..823594fbd0 100644 --- a/internal/api/grpc/oidc/v2/server.go +++ b/internal/api/grpc/oidc/v2/server.go @@ -1,7 +1,7 @@ package oidc import ( - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index 96426e8577..d2a81203ea 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -33,12 +33,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, return nil, status.Error(codes.Unauthenticated, "auth header missing") } - var orgDomain string - orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID) - if o, ok := req.(OrganisationFromRequest); ok { - orgID = o.OrganisationFromRequest().ID - orgDomain = o.OrganisationFromRequest().Domain - } + orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req) ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod) if err != nil { return nil, err @@ -47,11 +42,38 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, return handler(ctxSetter(ctx), req) } -type OrganisationFromRequest interface { - OrganisationFromRequest() *Organisation +func orgIDAndDomainFromRequest(ctx context.Context, req interface{}) (id, domain string) { + orgID := grpc_util.GetHeader(ctx, http.ZitadelOrgID) + o, ok := req.(OrganizationFromRequest) + if !ok { + return orgID, "" + } + id = o.OrganizationFromRequest().ID + domain = o.OrganizationFromRequest().Domain + if id != "" || domain != "" { + return id, domain + } + // check if the deprecated organisation is used. + // to be removed before going GA (https://github.com/zitadel/zitadel/issues/6718) + id = o.OrganisationFromRequest().ID + domain = o.OrganisationFromRequest().Domain + if id != "" || domain != "" { + return id, domain + } + return orgID, domain } -type Organisation struct { +// Deprecated: will be removed in favor of OrganizationFromRequest (https://github.com/zitadel/zitadel/issues/6718) +type OrganisationFromRequest interface { + OrganisationFromRequest() *Organization +} + +type Organization struct { ID string Domain string } + +type OrganizationFromRequest interface { + OrganizationFromRequest() *Organization + OrganisationFromRequest +} diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index fe8e0d8744..f98983936d 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -2,10 +2,13 @@ package session import ( "context" + "net" + "net/http" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/muhlemmer/gu" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/command" @@ -41,7 +44,7 @@ func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequ } func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) { - checks, metadata, err := s.createSessionRequestToCommand(ctx, req) + checks, metadata, userAgent, err := s.createSessionRequestToCommand(ctx, req) if err != nil { return nil, err } @@ -50,7 +53,7 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe return nil, err } - set, err := s.command.CreateSession(ctx, cmds, metadata) + set, err := s.command.CreateSession(ctx, cmds, metadata, userAgent) if err != nil { return nil, err } @@ -113,9 +116,34 @@ func sessionToPb(s *query.Session) *session.Session { Sequence: s.Sequence, Factors: factorsToPb(s), Metadata: s.Metadata, + UserAgent: userAgentToPb(s.UserAgent), } } +func userAgentToPb(ua domain.UserAgent) *session.UserAgent { + if ua.IsEmpty() { + return nil + } + + out := &session.UserAgent{ + FingerprintId: ua.FingerprintID, + Description: ua.Description, + } + if ua.IP != nil { + out.Ip = gu.Ptr(ua.IP.String()) + } + if ua.Header == nil { + return out + } + out.Header = make(map[string]*session.UserAgent_HeaderValues, len(ua.Header)) + for k, v := range ua.Header { + out.Header[k] = &session.UserAgent_HeaderValues{ + Values: v, + } + } + return out +} + func factorsToPb(s *query.Session) *session.Factors { user := userFactorToPb(s.UserFactor) if user == nil { @@ -188,6 +216,7 @@ func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor { LoginName: factor.LoginName, DisplayName: factor.DisplayName, OrganisationId: factor.ResourceOwner, + OrganizationId: factor.ResourceOwner, } } @@ -236,12 +265,30 @@ func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) { return query.NewSessionIDsSearchQuery(q.Ids) } -func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, error) { +func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, error) { checks, err := s.checksToCommand(ctx, req.Checks) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - return checks, req.GetMetadata(), nil + return checks, req.GetMetadata(), userAgentToCommand(req.GetUserAgent()), nil +} + +func userAgentToCommand(userAgent *session.UserAgent) *domain.UserAgent { + if userAgent == nil { + return nil + } + out := &domain.UserAgent{ + FingerprintID: userAgent.FingerprintId, + IP: net.ParseIP(userAgent.GetIp()), + Description: userAgent.Description, + } + if len(userAgent.Header) > 0 { + out.Header = make(http.Header, len(userAgent.Header)) + for k, values := range userAgent.Header { + out.Header[k] = values.GetValues() + } + } + return out } func (s *Server) setSessionRequestToCommand(ctx context.Context, req *session.SetSessionRequest) ([]command.SessionCommand, error) { diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index 1dcad7bc53..9aba59ee37 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" "github.com/zitadel/zitadel/internal/integration" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" @@ -53,7 +54,7 @@ func TestMain(m *testing.M) { }()) } -func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, factors ...wantFactor) *session.Session { +func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, factors ...wantFactor) *session.Session { t.Helper() require.NotEmpty(t, id) require.NotEmpty(t, token) @@ -70,6 +71,11 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window)) assert.Equal(t, sequence, s.GetSequence()) assert.Equal(t, metadata, s.GetMetadata()) + + if !proto.Equal(userAgent, s.GetUserAgent()) { + t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent) + } + verifyFactors(t, s.GetFactors(), window, factors) return s } @@ -131,11 +137,12 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, func TestServer_CreateSession(t *testing.T) { tests := []struct { - name string - req *session.CreateSessionRequest - want *session.CreateSessionResponse - wantErr bool - wantFactors []wantFactor + name string + req *session.CreateSessionRequest + want *session.CreateSessionResponse + wantErr bool + wantFactors []wantFactor + wantUserAgent *session.UserAgent }{ { name: "empty session", @@ -148,6 +155,33 @@ func TestServer_CreateSession(t *testing.T) { }, }, }, + { + name: "user agent", + req: &session.CreateSessionRequest{ + Metadata: map[string][]byte{"foo": []byte("bar")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, + want: &session.CreateSessionResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + wantUserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("Description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, + }, { name: "with user", req: &session.CreateSessionRequest{ @@ -219,7 +253,7 @@ func TestServer_CreateSession(t *testing.T) { require.NoError(t, err) integration.AssertDetails(t, tt.want, got) - verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantFactors...) + verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantFactors...) }) } } @@ -242,7 +276,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) require.NoError(t, err) @@ -258,7 +292,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactorUserVerified) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactorUserVerified) } func TestServer_CreateSession_successfulIntent(t *testing.T) { @@ -274,7 +308,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id") updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ @@ -288,7 +322,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantIntentFactor) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantIntentFactor) } func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { @@ -304,7 +338,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) idpUserID := "id" intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", idpUserID) @@ -331,7 +365,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantIntentFactor) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantIntentFactor) } func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { @@ -347,7 +381,7 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) intentID := Tester.CreateIntent(t, idpID) _, err = Client.SetSession(CTX, &session.SetSessionRequest{ @@ -399,7 +433,7 @@ func TestServer_SetSession_flow(t *testing.T) { createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) require.NoError(t, err) sessionToken := createResp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil) t.Run("check user", func(t *testing.T) { resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ @@ -415,7 +449,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor) }) t.Run("check webauthn, user verified (passkey)", func(t *testing.T) { @@ -430,7 +464,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) @@ -447,7 +481,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactorUserVerified) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactorUserVerified) }) userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken) @@ -474,7 +508,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false) @@ -491,7 +525,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor) }) } }) @@ -510,7 +544,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor) }) t.Run("check OTP SMS", func(t *testing.T) { @@ -522,7 +556,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() otp := resp.GetChallenges().GetOtpSms() @@ -539,7 +573,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor) }) t.Run("check OTP Email", func(t *testing.T) { @@ -553,7 +587,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil) + verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) sessionToken = resp.GetSessionToken() otp := resp.GetChallenges().GetOtpEmail() @@ -570,7 +604,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) sessionToken = resp.GetSessionToken() - verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor) + verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor) }) } diff --git a/internal/api/grpc/session/v2/session_test.go b/internal/api/grpc/session/v2/session_test.go index 33804caba5..8422b675b5 100644 --- a/internal/api/grpc/session/v2/session_test.go +++ b/internal/api/grpc/session/v2/session_test.go @@ -2,15 +2,19 @@ package session import ( "context" + "net" + "net/http" "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/query" @@ -23,7 +27,7 @@ func Test_sessionsToPb(t *testing.T) { past := now.Add(-time.Hour) sessions := []*query.Session{ - { // no factor + { // no factor, with user agent ID: "999", CreationDate: now, ChangeDate: now, @@ -32,6 +36,12 @@ func Test_sessionsToPb(t *testing.T) { ResourceOwner: "me", Creator: "he", Metadata: map[string][]byte{"hello": []byte("world")}, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerprintID"), + Description: gu.Ptr("description"), + IP: net.IPv4(1, 2, 3, 4), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, }, { // user factor ID: "999", @@ -114,13 +124,21 @@ func Test_sessionsToPb(t *testing.T) { } want := []*session.Session{ - { // no factor + { // no factor, with user agent Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), Sequence: 123, Factors: nil, Metadata: map[string][]byte{"hello": []byte("world")}, + UserAgent: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerprintID"), + Description: gu.Ptr("description"), + Ip: gu.Ptr("1.2.3.4"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + }, + }, }, { // user factor Id: "999", @@ -134,6 +152,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, }, Metadata: map[string][]byte{"hello": []byte("world")}, @@ -150,6 +169,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, Password: &session.PasswordFactor{ VerifiedAt: timestamppb.New(past), @@ -169,6 +189,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, WebAuthN: &session.WebAuthNFactor{ VerifiedAt: timestamppb.New(past), @@ -189,6 +210,7 @@ func Test_sessionsToPb(t *testing.T) { LoginName: "donald", DisplayName: "donald duck", OrganisationId: "org1", + OrganizationId: "org1", }, Totp: &session.TOTPFactor{ VerifiedAt: timestamppb.New(past), @@ -208,6 +230,71 @@ func Test_sessionsToPb(t *testing.T) { } } +func Test_userAgentToPb(t *testing.T) { + type args struct { + ua domain.UserAgent + } + tests := []struct { + name string + args args + want *session.UserAgent + }{ + { + name: "empty", + args: args{domain.UserAgent{}}, + }, + { + name: "fingerprint id and description", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + }, + }, + { + name: "with ip", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + IP: net.IPv4(1, 2, 3, 4), + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Ip: gu.Ptr("1.2.3.4"), + }, + }, + { + name: "with header", + args: args{domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Header: http.Header{ + "foo": []string{"foo", "bar"}, + "hello": []string{"world"}, + }, + }}, + want: &session.UserAgent{ + FingerprintId: gu.Ptr("fingerPrintID"), + Description: gu.Ptr("description"), + Header: map[string]*session.UserAgent_HeaderValues{ + "foo": {Values: []string{"foo", "bar"}}, + "hello": {Values: []string{"world"}}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := userAgentToPb(tt.args.ua) + assert.Equal(t, tt.want, got) + }) + } +} + func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery { q, err := query.NewTextQuery(column, value, compare) require.NoError(t, err) @@ -510,3 +597,73 @@ func Test_userVerificationRequirementToDomain(t *testing.T) { }) } } + +func Test_userAgentToCommand(t *testing.T) { + type args struct { + userAgent *session.UserAgent + } + tests := []struct { + name string + args args + want *domain.UserAgent + }{ + { + name: "nil", + args: args{nil}, + want: nil, + }, + { + name: "all fields", + args: args{&session.UserAgent{ + FingerprintId: gu.Ptr("fp1"), + Ip: gu.Ptr("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: map[string]*session.UserAgent_HeaderValues{ + "hello": { + Values: []string{"foo", "bar"}, + }, + }, + }}, + want: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{ + "hello": []string{"foo", "bar"}, + }, + }, + }, + { + name: "invalid ip", + args: args{&session.UserAgent{ + FingerprintId: gu.Ptr("fp1"), + Ip: gu.Ptr("oops"), + Description: gu.Ptr("firefox"), + Header: map[string]*session.UserAgent_HeaderValues{ + "hello": { + Values: []string{"foo", "bar"}, + }, + }, + }}, + want: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: nil, + Description: gu.Ptr("firefox"), + Header: http.Header{ + "hello": []string{"foo", "bar"}, + }, + }, + }, + { + name: "nil fields", + args: args{&session.UserAgent{}}, + want: &domain.UserAgent{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := userAgentToCommand(tt.args.userAgent) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 4eea0a18f7..551079aec5 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -3,7 +3,7 @@ package system import ( "strings" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/grpc/authn" diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 4ba077d043..56c0902b11 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -7,8 +7,8 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 5999f21e5e..8fbd18530d 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/api/oidc/auth_request_converter_v2.go b/internal/api/oidc/auth_request_converter_v2.go index 9f4c0d0a1f..3a35b01578 100644 --- a/internal/api/oidc/auth_request_converter_v2.go +++ b/internal/api/oidc/auth_request_converter_v2.go @@ -3,7 +3,7 @@ package oidc import ( "time" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/command" ) diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go index 532814bee1..5b013fe864 100644 --- a/internal/api/oidc/auth_request_integration_test.go +++ b/internal/api/oidc/auth_request_integration_test.go @@ -11,8 +11,8 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" http_utils "github.com/zitadel/zitadel/internal/api/http" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" @@ -103,7 +103,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) { assert.Equal(t, "state", values.Get("state")) // check id_token / claims - provider, err := Tester.CreateRelyingParty(clientID, redirectURIImplicit) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURIImplicit) require.NoError(t, err) claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier()) require.NoError(t, err) @@ -147,7 +147,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) { func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -177,13 +177,13 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) { assertIDTokenClaims(t, newTokens.IDTokenClaims, armPasskey, startTime, changeTime) // refresh with an old refresh_token must fail - _, err = rp.RefreshAccessToken(provider, tokens.RefreshToken, "", "") + _, err = rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, tokens.RefreshToken, "", "") require.Error(t, err) } func TestOPStorage_RevokeToken_access_token(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -206,11 +206,11 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke access token - err = rp.RevokeToken(provider, tokens.AccessToken, "access_token") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "access_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh grant must still work @@ -218,15 +218,15 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) { require.NoError(t, err) // revocation with the same access token must not fail (with or without hint) - err = rp.RevokeToken(provider, tokens.AccessToken, "access_token") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "access_token") require.NoError(t, err) - err = rp.RevokeToken(provider, tokens.AccessToken, "") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "") require.NoError(t, err) } func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -249,11 +249,11 @@ func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke access token - err = rp.RevokeToken(provider, tokens.AccessToken, "refresh_token") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "refresh_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh grant must still work @@ -263,7 +263,7 @@ func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -286,11 +286,11 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke refresh token -> invalidates also access token - err = rp.RevokeToken(provider, tokens.RefreshToken, "refresh_token") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "refresh_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh must fail @@ -298,15 +298,15 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { require.Error(t, err) // revocation with the same refresh token must not fail (with or without hint) - err = rp.RevokeToken(provider, tokens.RefreshToken, "refresh_token") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "refresh_token") require.NoError(t, err) - err = rp.RevokeToken(provider, tokens.RefreshToken, "") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "") require.NoError(t, err) } func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -329,11 +329,11 @@ func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing. assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // revoke refresh token even with a wrong hint - err = rp.RevokeToken(provider, tokens.RefreshToken, "access_token") + err = rp.RevokeToken(CTX, provider, tokens.RefreshToken, "access_token") require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) // refresh must fail @@ -365,15 +365,15 @@ func TestOPStorage_RevokeToken_invalid_client(t *testing.T) { // simulate second client (not part of the audience) trying to revoke the token otherClientID := createClient(t) - provider, err := Tester.CreateRelyingParty(otherClientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, otherClientID, redirectURI) require.NoError(t, err) - err = rp.RevokeToken(provider, tokens.AccessToken, "") + err = rp.RevokeToken(CTX, provider, tokens.AccessToken, "") require.Error(t, err) } func TestOPStorage_TerminateSession(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -396,21 +396,21 @@ func TestOPStorage_TerminateSession(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // userinfo must not fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) - postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) } func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -433,28 +433,28 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // userinfo must not fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) - postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) refreshedTokens, err := refreshTokens(t, clientID, tokens.RefreshToken) require.NoError(t, err) // userinfo must not fail - _, err = rp.Userinfo(refreshedTokens.AccessToken, refreshedTokens.TokenType, refreshedTokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, refreshedTokens.AccessToken, refreshedTokens.TokenType, refreshedTokens.IDTokenClaims.Subject, provider) require.NoError(t, err) } func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -476,12 +476,12 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { assertTokens(t, tokens, false) assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) - postLogoutRedirect, err := rp.EndSession(provider, "", logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, http_utils.BuildOrigin(Tester.Host(), Tester.Config.ExternalSecure)+Tester.Config.OIDC.DefaultLogoutURLV2+logoutRedirectURI+"?state=state", postLogoutRedirect.String()) // userinfo must not fail until login UI terminated session - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) // simulate termination by login UI @@ -492,12 +492,12 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { require.NoError(t, err) // userinfo must fail - _, err = rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.Error(t, err) } func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDTokenClaims], error) { - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) codeVerifier := "codeVerifier" @@ -505,23 +505,10 @@ func exchangeTokens(t testing.TB, clientID, code string) (*oidc.Tokens[*oidc.IDT } func refreshTokens(t testing.TB, clientID, refreshToken string) (*oidc.Tokens[*oidc.IDTokenClaims], error) { - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) - tokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "") - if err != nil { - return nil, err - } - idToken, _ := tokens.Extra("id_token").(string) - claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), tokens.AccessToken, idToken, provider.IDTokenVerifier()) - if err != nil { - return nil, err - } - return &oidc.Tokens[*oidc.IDTokenClaims]{ - Token: tokens, - IDToken: idToken, - IDTokenClaims: claims, - }, nil + return rp.RefreshTokens[*oidc.IDTokenClaims](CTX, provider, refreshToken, "", "") } func assertCodeResponse(t *testing.T, callback string) string { diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 6a8dc6be6e..7c56288171 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -9,10 +9,10 @@ import ( "time" "github.com/dop251/goja" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/actions/object" @@ -564,7 +564,7 @@ func (o *OPStorage) userinfoFlows(ctx context.Context, user *query.User, userGra apiFields, action.Script, action.Name, - append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -745,7 +745,7 @@ func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, userG apiFields, action.Script, action.Name, - append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index 5f3f25f759..ec208db27c 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -4,8 +4,8 @@ import ( "strings" "time" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/api/oidc/client_credentials.go b/internal/api/oidc/client_credentials.go index fda1f6c94a..3c2f272ead 100644 --- a/internal/api/oidc/client_credentials.go +++ b/internal/api/oidc/client_credentials.go @@ -3,8 +3,8 @@ package oidc import ( "time" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" ) type clientCredentialsRequest struct { diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go index 11121a8fae..8a7f58a441 100644 --- a/internal/api/oidc/client_integration_test.go +++ b/internal/api/oidc/client_integration_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/client/rs" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" @@ -41,9 +41,9 @@ func TestOPStorage_SetUserinfoFromToken(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // test actual userinfo - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) - userinfo, err := rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) + userinfo, err := rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) assertUserinfo(t, userinfo) } @@ -62,7 +62,7 @@ func TestOPStorage_SetIntrospectionFromToken(t *testing.T) { ExpirationDate: nil, }) require.NoError(t, err) - resourceServer, err := Tester.CreateResourceServer(keyResp.GetKeyDetails()) + resourceServer, err := Tester.CreateResourceServer(CTX, keyResp.GetKeyDetails()) require.NoError(t, err) scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess} @@ -87,7 +87,7 @@ func TestOPStorage_SetIntrospectionFromToken(t *testing.T) { assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime) // test actual introspection - introspection, err := rs.Introspect(context.Background(), resourceServer, tokens.AccessToken) + introspection, err := rs.Introspect[*oidc.IntrospectionResponse](context.Background(), resourceServer, tokens.AccessToken) require.NoError(t, err) assertIntrospection(t, introspection, Tester.OIDCIssuer(), app.GetClientId(), diff --git a/internal/api/oidc/device_auth.go b/internal/api/oidc/device_auth.go index 7eee06096a..80298e8afd 100644 --- a/internal/api/oidc/device_auth.go +++ b/internal/api/oidc/device_auth.go @@ -5,8 +5,8 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/api/oidc/jwt-profile.go b/internal/api/oidc/jwt-profile.go index 47805783c9..e2592a5734 100644 --- a/internal/api/oidc/jwt-profile.go +++ b/internal/api/oidc/jwt-profile.go @@ -3,8 +3,8 @@ package oidc import ( "context" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 82884101d6..3a2a6ae32c 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -5,9 +5,9 @@ import ( "fmt" "time" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go index 1a1516040d..7738e3d615 100644 --- a/internal/api/oidc/oidc_integration_test.go +++ b/internal/api/oidc/oidc_integration_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/grpc/metadata" "github.com/zitadel/zitadel/internal/domain" @@ -216,7 +216,7 @@ func Test_ZITADEL_API_inactive_access_token(t *testing.T) { func Test_ZITADEL_API_terminated_session(t *testing.T) { clientID := createClient(t) - provider, err := Tester.CreateRelyingParty(clientID, redirectURI) + provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) @@ -245,7 +245,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) { require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId()) // refresh token - postLogoutRedirect, err := rp.EndSession(provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) @@ -271,13 +271,13 @@ func createImplicitClient(t testing.TB) string { } func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string { - redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) + redURL, err := Tester.CreateOIDCAuthRequest(CTX, clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) require.NoError(t, err) return redURL } func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string { - redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) + redURL, err := Tester.CreateOIDCAuthRequestImplicit(CTX, clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...) require.NoError(t, err) return redURL } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index da96302e60..c165b117b9 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -7,8 +7,8 @@ import ( "time" "github.com/rakyll/statik/fs" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/assets" diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index d2bf3bf3e2..6dd02e1bc2 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -5,9 +5,9 @@ import ( "fmt" "time" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" "github.com/zitadel/saml/pkg/provider/key" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 7e00605b7b..6f632136f0 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -15,7 +15,7 @@ import ( "github.com/gorilla/mux" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/cmd/build" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 516f7bc3d0..f7287dd1d2 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -7,7 +7,7 @@ import ( "github.com/dop251/goja" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/actions" @@ -133,7 +133,7 @@ func (l *Login) runPostExternalAuthenticationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -206,7 +206,7 @@ func (l *Login) runPostInternalAuthenticationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -307,7 +307,7 @@ func (l *Login) runPreCreationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { @@ -365,7 +365,7 @@ func (l *Login) runPostCreationActions( apiFields, a.Script, a.Name, - append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx))..., + append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., ) cancel() if err != nil { diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index c9cc94203f..d9c8c04fc0 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -7,8 +7,8 @@ import ( "github.com/crewjam/saml/samlsp" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/api/ui/login/jwt_handler.go b/internal/api/ui/login/jwt_handler.go index 51b0795843..aa7a4466dc 100644 --- a/internal/api/ui/login/jwt_handler.go +++ b/internal/api/ui/login/jwt_handler.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" http_util "github.com/zitadel/zitadel/internal/api/http" diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index d67b2dcc7b..42a9ba350b 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -7,10 +7,10 @@ import ( "strings" "time" + "github.com/go-jose/go-jose/v3" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" - "github.com/zitadel/oidc/v2/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" @@ -274,7 +274,7 @@ func (repo *TokenVerifierRepo) getTokenIDAndSubject(ctx context.Context, accessT return splitToken[0], splitToken[1], true } -func (repo *TokenVerifierRepo) jwtTokenVerifier(ctx context.Context) op.AccessTokenVerifier { +func (repo *TokenVerifierRepo) jwtTokenVerifier(ctx context.Context) *op.AccessTokenVerifier { keySet := &openIDKeySet{repo.Query} issuer := http_util.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), repo.ExternalSecure) return op.NewAccessTokenVerifier(issuer, keySet) diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 0c1bb1cf31..183e1b57c1 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -2,6 +2,8 @@ package command import ( "context" + "net" + "net/http" "testing" "time" @@ -354,7 +356,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), ), ), tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { @@ -397,7 +407,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), ), ), tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { @@ -440,8 +458,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate), - ), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "userID", testNow), @@ -517,8 +542,15 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate), - ), + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "userID", testNow), diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 265293a3a8..edf28ef460 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -9,7 +9,7 @@ import ( "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 4f5b35c339..400b0faf0e 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -9,7 +9,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/command/idp_model.go b/internal/command/idp_model.go index b4d932fb2b..e9949fb406 100644 --- a/internal/command/idp_model.go +++ b/internal/command/idp_model.go @@ -7,7 +7,7 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rp" "golang.org/x/oauth2" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/command/instance_idp_test.go b/internal/command/instance_idp_test.go index 3e45d82666..8989f4c92b 100644 --- a/internal/command/instance_idp_test.go +++ b/internal/command/instance_idp_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index ab850aeea1..a517814e00 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -2,6 +2,8 @@ package command import ( "context" + "net" + "net/http" "testing" "time" @@ -163,7 +165,15 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), ), eventFromEventPusher( session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, @@ -356,7 +366,15 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) { ), expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), ), eventFromEventPusher( session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, diff --git a/internal/command/org_idp_test.go b/internal/command/org_idp_test.go index d6eef4a399..8f966e7402 100644 --- a/internal/command/org_idp_test.go +++ b/internal/command/org_idp_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 6db8654d90..6f00765bcc 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -7,7 +7,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" diff --git a/internal/command/session.go b/internal/command/session.go index a58c5d2d3a..caf3056f76 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -166,8 +166,8 @@ func (s *SessionCommands) Exec(ctx context.Context) error { return nil } -func (s *SessionCommands) Start(ctx context.Context) { - s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate)) +func (s *SessionCommands) Start(ctx context.Context, userAgent *domain.UserAgent) { + s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate, userAgent)) } func (s *SessionCommands) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error { @@ -280,7 +280,7 @@ func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Co return token, s.eventCommands, nil } -func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte) (set *SessionChanged, err error) { +func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte, userAgent *domain.UserAgent) (set *SessionChanged, err error) { sessionID, err := c.idGenerator.Next() if err != nil { return nil, err @@ -291,7 +291,7 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, met return nil, err } cmd := c.NewSessionCommands(cmds, sessionWriteModel) - cmd.Start(ctx) + cmd.Start(ctx, userAgent) return c.updateSession(ctx, cmd, metadata) } diff --git a/internal/command/session_test.go b/internal/command/session_test.go index e478792e1e..7e443a4c53 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -3,10 +3,13 @@ package command import ( "context" "io" + "net" + "net/http" "testing" "time" "github.com/golang/mock/gomock" + "github.com/muhlemmer/gu" "github.com/pquerna/otp/totp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -145,9 +148,10 @@ func TestCommands_CreateSession(t *testing.T) { tokenCreator func(sessionID string) (string, string, error) } type args struct { - ctx context.Context - checks []SessionCommand - metadata map[string][]byte + ctx context.Context + checks []SessionCommand + metadata map[string][]byte + userAgent *domain.UserAgent } type res struct { want *SessionChanged @@ -200,11 +204,25 @@ func TestCommands_CreateSession(t *testing.T) { }, args{ ctx: authz.NewMockContext("", "org1", ""), + userAgent: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, }, []expect{ expectFilter(), expectPush( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID", ), @@ -227,7 +245,7 @@ func TestCommands_CreateSession(t *testing.T) { idGenerator: tt.fields.idGenerator, sessionTokenCreator: tt.fields.tokenCreator, } - got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata) + got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata, tt.args.userAgent) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.want, got) }) @@ -276,7 +294,15 @@ func TestCommands_UpdateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -301,7 +327,15 @@ func TestCommands_UpdateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -866,7 +900,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -891,7 +933,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID")), @@ -920,7 +970,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID"), @@ -950,7 +1008,15 @@ func TestCommands_TerminateSession(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter( eventFromEventPusher( - session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)), + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "org1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), eventFromEventPusher( session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "tokenID"), diff --git a/internal/command/user_human_refresh_token_test.go b/internal/command/user_human_refresh_token_test.go index 6ad7f68654..c623c737b0 100644 --- a/internal/command/user_human_refresh_token_test.go +++ b/internal/command/user_human_refresh_token_test.go @@ -8,7 +8,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index a5a293a449..479d5731e4 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -147,7 +147,7 @@ func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string, func (c *HasherConfig) decodeParams(dst any) error { decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - ErrorUnused: true, + ErrorUnused: false, ErrorUnset: true, Result: dst, }) diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index b557ca4a5c..0538ac631a 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -379,7 +379,10 @@ func TestHasherConfig_decodeParams(t *testing.T) { "b": 2, "c": 3, }, - wantErr: true, + want: dst{ + A: 1, + B: 2, + }, }, { name: "unset", diff --git a/internal/domain/user_agent.go b/internal/domain/user_agent.go new file mode 100644 index 0000000000..ca72c6bec4 --- /dev/null +++ b/internal/domain/user_agent.go @@ -0,0 +1,17 @@ +package domain + +import ( + "net" + httplib "net/http" +) + +type UserAgent struct { + FingerprintID *string `json:"fingerprint_id,omitempty"` + IP net.IP `json:"ip,omitempty"` + Description *string `json:"description,omitempty"` + Header httplib.Header `json:"header,omitempty"` +} + +func (ua UserAgent) IsEmpty() bool { + return ua.FingerprintID == nil && len(ua.IP) == 0 && ua.Description == nil && ua.Header == nil +} diff --git a/internal/idp/providers/apple/apple.go b/internal/idp/providers/apple/apple.go index 9463cb61bf..9664e006c8 100644 --- a/internal/idp/providers/apple/apple.go +++ b/internal/idp/providers/apple/apple.go @@ -5,9 +5,9 @@ import ( "encoding/pem" "time" - "github.com/zitadel/oidc/v2/pkg/crypto" - openid "github.com/zitadel/oidc/v2/pkg/oidc" - "gopkg.in/square/go-jose.v2" + "github.com/go-jose/go-jose/v3" + "github.com/zitadel/oidc/v3/pkg/crypto" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go index ae629f9573..5e9143f050 100644 --- a/internal/idp/providers/apple/session.go +++ b/internal/idp/providers/apple/session.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/apple/session_test.go b/internal/idp/providers/apple/session_test.go index 207f219813..3c1ab59763 100644 --- a/internal/idp/providers/apple/session_test.go +++ b/internal/idp/providers/apple/session_test.go @@ -9,7 +9,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/azuread/azuread.go b/internal/idp/providers/azuread/azuread.go index 244f383e1c..46445a3977 100644 --- a/internal/idp/providers/azuread/azuread.go +++ b/internal/idp/providers/azuread/azuread.go @@ -3,7 +3,7 @@ package azuread import ( "fmt" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/azuread/azuread_test.go b/internal/idp/providers/azuread/azuread_test.go index 3febb43f95..122a70bb07 100644 --- a/internal/idp/providers/azuread/azuread_test.go +++ b/internal/idp/providers/azuread/azuread_test.go @@ -6,8 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oauth" diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index 5bc7bb84c9..698cdad198 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -3,8 +3,8 @@ package azuread import ( "net/http" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp/providers/oauth" ) diff --git a/internal/idp/providers/azuread/session_test.go b/internal/idp/providers/azuread/session_test.go index 531d909b92..f68c4cc7d7 100644 --- a/internal/idp/providers/azuread/session_test.go +++ b/internal/idp/providers/azuread/session_test.go @@ -10,7 +10,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/github/session_test.go b/internal/idp/providers/github/session_test.go index 2ad9f449fc..247ef35c68 100644 --- a/internal/idp/providers/github/session_test.go +++ b/internal/idp/providers/github/session_test.go @@ -10,7 +10,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/gitlab/gitlab.go b/internal/idp/providers/gitlab/gitlab.go index 1bb02302f3..76e9a74b40 100644 --- a/internal/idp/providers/gitlab/gitlab.go +++ b/internal/idp/providers/gitlab/gitlab.go @@ -1,7 +1,7 @@ package gitlab import ( - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/gitlab/session_test.go b/internal/idp/providers/gitlab/session_test.go index de59044326..0853f3ce3e 100644 --- a/internal/idp/providers/gitlab/session_test.go +++ b/internal/idp/providers/gitlab/session_test.go @@ -9,8 +9,8 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/google/google.go b/internal/idp/providers/google/google.go index 7036d4e101..221f2b61ae 100644 --- a/internal/idp/providers/google/google.go +++ b/internal/idp/providers/google/google.go @@ -1,7 +1,7 @@ package google import ( - openid "github.com/zitadel/oidc/v2/pkg/oidc" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/oidc" diff --git a/internal/idp/providers/google/session_test.go b/internal/idp/providers/google/session_test.go index d3edde0ea3..1915adf1bc 100644 --- a/internal/idp/providers/google/session_test.go +++ b/internal/idp/providers/google/session_test.go @@ -9,8 +9,8 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - openid "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + openid "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index be3ffc0531..54fcc039eb 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -8,8 +8,8 @@ import ( "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/idp/providers/jwt/session_test.go b/internal/idp/providers/jwt/session_test.go index ae79ed22e5..3a8210aec8 100644 --- a/internal/idp/providers/jwt/session_test.go +++ b/internal/idp/providers/jwt/session_test.go @@ -7,14 +7,14 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3" "github.com/golang/mock/gomock" "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/idp/providers/oauth/oauth2.go b/internal/idp/providers/oauth/oauth2.go index a31e9d4c26..ad8cbbd45b 100644 --- a/internal/idp/providers/oauth/oauth2.go +++ b/internal/idp/providers/oauth/oauth2.go @@ -3,8 +3,8 @@ package oauth import ( "context" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "github.com/zitadel/zitadel/internal/idp" diff --git a/internal/idp/providers/oauth/oauth2_test.go b/internal/idp/providers/oauth/oauth2_test.go index 5fdfbc2185..814a7ac9c2 100644 --- a/internal/idp/providers/oauth/oauth2_test.go +++ b/internal/idp/providers/oauth/oauth2_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rp" "golang.org/x/oauth2" "github.com/zitadel/zitadel/internal/idp" diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index eb9ab49efe..065ad3b213 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -5,9 +5,9 @@ import ( "errors" "net/http" - "github.com/zitadel/oidc/v2/pkg/client/rp" - httphelper "github.com/zitadel/oidc/v2/pkg/http" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" ) diff --git a/internal/idp/providers/oauth/session_test.go b/internal/idp/providers/oauth/session_test.go index 9901ea11c3..ce6fa3f10f 100644 --- a/internal/idp/providers/oauth/session_test.go +++ b/internal/idp/providers/oauth/session_test.go @@ -9,7 +9,7 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" diff --git a/internal/idp/providers/oidc/oidc.go b/internal/idp/providers/oidc/oidc.go index aab5255488..30d4d11abf 100644 --- a/internal/idp/providers/oidc/oidc.go +++ b/internal/idp/providers/oidc/oidc.go @@ -3,8 +3,8 @@ package oidc import ( "context" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" ) @@ -100,7 +100,7 @@ func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []stri for _, option := range options { option(provider) } - provider.RelyingParty, err = rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, setDefaultScope(scopes), provider.options...) + provider.RelyingParty, err = rp.NewRelyingPartyOIDC(context.TODO(), issuer, clientID, clientSecret, redirectURI, setDefaultScope(scopes), provider.options...) if err != nil { return nil, err } diff --git a/internal/idp/providers/oidc/oidc_test.go b/internal/idp/providers/oidc/oidc_test.go index bbe08155c8..d510bf15c2 100644 --- a/internal/idp/providers/oidc/oidc_test.go +++ b/internal/idp/providers/oidc/oidc_test.go @@ -7,8 +7,8 @@ import ( "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/zitadel/internal/idp" ) diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index 366e42643a..bb44fb1146 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/domain" @@ -38,7 +38,7 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return nil, err } } - info, err := rp.Userinfo( + info, err := rp.Userinfo[*oidc.UserInfo](ctx, s.Tokens.AccessToken, s.Tokens.TokenType, s.Tokens.IDTokenClaims.GetSubject(), diff --git a/internal/idp/providers/oidc/session_test.go b/internal/idp/providers/oidc/session_test.go index afaa358042..200dac8fc4 100644 --- a/internal/idp/providers/oidc/session_test.go +++ b/internal/idp/providers/oidc/session_test.go @@ -7,14 +7,14 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3" "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" - "gopkg.in/square/go-jose.v2" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" diff --git a/internal/integration/client.go b/internal/integration/client.go index 71bc9805c4..98a577be14 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -9,7 +9,7 @@ import ( crewjam_saml "github.com/crewjam/saml" "github.com/stretchr/testify/require" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" "google.golang.org/grpc" diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 3e270b1d72..61d7f9d90d 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -19,8 +19,8 @@ import ( "github.com/spf13/viper" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index ad1a44de32..b6edcd3aea 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "github.com/zitadel/oidc/v2/pkg/client" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/client/rs" - "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/client/rs" + "github.com/zitadel/oidc/v3/pkg/oidc" http_util "github.com/zitadel/zitadel/internal/api/http" oidc_internal "github.com/zitadel/zitadel/internal/api/oidc" @@ -83,8 +83,8 @@ func (s *Tester) CreateAPIClient(ctx context.Context, projectID string) (*manage }) } -func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...) +func (s *Tester) CreateOIDCAuthRequest(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) if err != nil { return "", err } @@ -110,8 +110,8 @@ func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string return strings.TrimPrefix(loc.String(), prefixWithHost), nil } -func (s *Tester) CreateOIDCAuthRequestImplicit(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { - provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...) +func (s *Tester) CreateOIDCAuthRequestImplicit(ctx context.Context, clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) { + provider, err := s.CreateRelyingParty(ctx, clientID, redirectURI, scope...) if err != nil { return "", err } @@ -146,12 +146,12 @@ func (s *Tester) OIDCIssuer() string { return http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure) } -func (s *Tester) CreateRelyingParty(clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { +func (s *Tester) CreateRelyingParty(ctx context.Context, clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) { if len(scope) == 0 { scope = []string{oidc.ScopeOpenID} } loginClient := &http.Client{Transport: &loginRoundTripper{http.DefaultTransport}} - return rp.NewRelyingPartyOIDC(s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) + return rp.NewRelyingPartyOIDC(ctx, s.OIDCIssuer(), clientID, "", redirectURI, scope, rp.WithHTTPClient(loginClient)) } type loginRoundTripper struct { @@ -163,12 +163,12 @@ func (c *loginRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) return c.RoundTripper.RoundTrip(req) } -func (s *Tester) CreateResourceServer(keyFileData []byte) (rs.ResourceServer, error) { +func (s *Tester) CreateResourceServer(ctx context.Context, keyFileData []byte) (rs.ResourceServer, error) { keyFile, err := client.ConfigFromKeyFileData(keyFileData) if err != nil { return nil, err } - return rs.NewResourceServerJWTProfile(s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key)) + return rs.NewResourceServerJWTProfile(ctx, s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key)) } func GetRequest(url string, headers map[string]string) (*http.Request, error) { diff --git a/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl b/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl index bc0123c750..adb71c42ff 100644 --- a/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl +++ b/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl @@ -17,8 +17,8 @@ var {{.ServiceName}}_AuthMethods = authz.MethodMapping { } {{ range $m := .AuthContext}} -func (r *{{ $m.Name }}) OrganisationFromRequest() *middleware.Organisation { - return &middleware.Organisation{ +func (r *{{ $m.Name }}) OrganizationFromRequest() *middleware.Organization { + return &middleware.Organization{ ID: r{{$m.OrgMethod}}.GetOrgId(), Domain: r{{$m.OrgMethod}}.GetOrgDomain(), } diff --git a/internal/query/prepare_test.go b/internal/query/prepare_test.go index 242b387408..c9770c0dd1 100644 --- a/internal/query/prepare_test.go +++ b/internal/query/prepare_test.go @@ -54,7 +54,7 @@ func assertPrepare(t *testing.T, prepareFunc, expectedObject interface{}, sqlExp } return isErr(err) } - object, ok, didScan := execScan(&database.DB{DB: client}, builder, scan, errCheck) + object, ok, didScan := execScan(t, &database.DB{DB: client}, builder, scan, errCheck) if !ok { t.Error(object) return false @@ -168,7 +168,7 @@ var ( selectBuilderType = reflect.TypeOf(sq.SelectBuilder{}) ) -func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, errCheck checkErr) (object interface{}, ok bool, didScan bool) { +func execScan(t testing.TB, client *database.DB, builder sq.SelectBuilder, scan interface{}, errCheck checkErr) (object interface{}, ok bool, didScan bool) { scanType := reflect.TypeOf(scan) err := validateScan(scanType) if err != nil { @@ -177,7 +177,7 @@ func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, e stmt, args, err := builder.ToSql() if err != nil { - return fmt.Errorf("unexpeted error from sql builder: %w", err), false, false + return fmt.Errorf("unexpected error from sql builder: %w", err), false, false } //resultSet represents *sql.Row or *sql.Rows, @@ -199,6 +199,9 @@ func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, e // if scan(*sql.Row)... } else if scanType.In(0).AssignableTo(rowType) { err = client.QueryRow(func(r *sql.Row) error { + if r.Err() != nil { + return r.Err() + } didScan = true res = reflect.ValueOf(scan).Call([]reflect.Value{reflect.ValueOf(r)}) if err, ok := res[1].Interface().(error); ok { @@ -213,6 +216,9 @@ func execScan(client *database.DB, builder sq.SelectBuilder, scan interface{}, e if err != nil { err, ok := errCheck(err) + if !ok { + t.Fatal(err) + } if didScan { return res[0].Interface(), ok, didScan } diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go index 816969d662..5bfc5dd8c9 100644 --- a/internal/query/projection/session.go +++ b/internal/query/projection/session.go @@ -14,27 +14,31 @@ import ( ) const ( - SessionsProjectionTable = "projections.sessions5" + SessionsProjectionTable = "projections.sessions6" - SessionColumnID = "id" - SessionColumnCreationDate = "creation_date" - SessionColumnChangeDate = "change_date" - SessionColumnSequence = "sequence" - SessionColumnState = "state" - SessionColumnResourceOwner = "resource_owner" - SessionColumnInstanceID = "instance_id" - SessionColumnCreator = "creator" - SessionColumnUserID = "user_id" - SessionColumnUserCheckedAt = "user_checked_at" - SessionColumnPasswordCheckedAt = "password_checked_at" - SessionColumnIntentCheckedAt = "intent_checked_at" - SessionColumnWebAuthNCheckedAt = "webauthn_checked_at" - SessionColumnWebAuthNUserVerified = "webauthn_user_verified" - SessionColumnTOTPCheckedAt = "totp_checked_at" - SessionColumnOTPSMSCheckedAt = "otp_sms_checked_at" - SessionColumnOTPEmailCheckedAt = "otp_email_checked_at" - SessionColumnMetadata = "metadata" - SessionColumnTokenID = "token_id" + SessionColumnID = "id" + SessionColumnCreationDate = "creation_date" + SessionColumnChangeDate = "change_date" + SessionColumnSequence = "sequence" + SessionColumnState = "state" + SessionColumnResourceOwner = "resource_owner" + SessionColumnInstanceID = "instance_id" + SessionColumnCreator = "creator" + SessionColumnUserID = "user_id" + SessionColumnUserCheckedAt = "user_checked_at" + SessionColumnPasswordCheckedAt = "password_checked_at" + SessionColumnIntentCheckedAt = "intent_checked_at" + SessionColumnWebAuthNCheckedAt = "webauthn_checked_at" + SessionColumnWebAuthNUserVerified = "webauthn_user_verified" + SessionColumnTOTPCheckedAt = "totp_checked_at" + SessionColumnOTPSMSCheckedAt = "otp_sms_checked_at" + SessionColumnOTPEmailCheckedAt = "otp_email_checked_at" + SessionColumnMetadata = "metadata" + SessionColumnTokenID = "token_id" + SessionColumnUserAgentFingerprintID = "user_agent_fingerprint_id" + SessionColumnUserAgentIP = "user_agent_ip" + SessionColumnUserAgentDescription = "user_agent_description" + SessionColumnUserAgentHeader = "user_agent_header" ) type sessionProjection struct{} @@ -69,8 +73,16 @@ func (*sessionProjection) Init() *old_handler.Check { handler.NewColumn(SessionColumnOTPEmailCheckedAt, handler.ColumnTypeTimestamp, handler.Nullable()), handler.NewColumn(SessionColumnMetadata, handler.ColumnTypeJSONB, handler.Nullable()), handler.NewColumn(SessionColumnTokenID, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SessionColumnUserAgentFingerprintID, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SessionColumnUserAgentIP, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SessionColumnUserAgentDescription, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(SessionColumnUserAgentHeader, handler.ColumnTypeJSONB, handler.Nullable()), }, handler.NewPrimaryKey(SessionColumnInstanceID, SessionColumnID), + handler.WithIndex(handler.NewIndex( + SessionColumnUserAgentFingerprintID+"_idx", + []string{SessionColumnUserAgentFingerprintID}, + )), ), ) } @@ -153,19 +165,35 @@ func (p *sessionProjection) reduceSessionAdded(event eventstore.Event) (*handler return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Sfrgf", "reduce.wrong.event.type %s", session.AddedType) } - return handler.NewCreateStatement( - e, - []handler.Column{ - handler.NewCol(SessionColumnID, e.Aggregate().ID), - handler.NewCol(SessionColumnInstanceID, e.Aggregate().InstanceID), - handler.NewCol(SessionColumnCreationDate, e.CreationDate()), - handler.NewCol(SessionColumnChangeDate, e.CreationDate()), - handler.NewCol(SessionColumnResourceOwner, e.Aggregate().ResourceOwner), - handler.NewCol(SessionColumnState, domain.SessionStateActive), - handler.NewCol(SessionColumnSequence, e.Sequence()), - handler.NewCol(SessionColumnCreator, e.User), - }, - ), nil + cols := make([]handler.Column, 0, 12) + cols = append(cols, + handler.NewCol(SessionColumnID, e.Aggregate().ID), + handler.NewCol(SessionColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(SessionColumnCreationDate, e.CreationDate()), + handler.NewCol(SessionColumnChangeDate, e.CreationDate()), + handler.NewCol(SessionColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(SessionColumnState, domain.SessionStateActive), + handler.NewCol(SessionColumnSequence, e.Sequence()), + handler.NewCol(SessionColumnCreator, e.User), + ) + if e.UserAgent != nil { + cols = append(cols, + handler.NewCol(SessionColumnUserAgentFingerprintID, e.UserAgent.FingerprintID), + handler.NewCol(SessionColumnUserAgentDescription, e.UserAgent.Description), + ) + if e.UserAgent.IP != nil { + cols = append(cols, + handler.NewCol(SessionColumnUserAgentIP, e.UserAgent.IP.String()), + ) + } + if e.UserAgent.Header != nil { + cols = append(cols, + handler.NewJSONCol(SessionColumnUserAgentHeader, e.UserAgent.Header), + ) + } + } + + return handler.NewCreateStatement(e, cols), nil } func (p *sessionProjection) reduceUserChecked(event eventstore.Event) (*handler.Statement, error) { diff --git a/internal/query/projection/session_test.go b/internal/query/projection/session_test.go index 607b028ff4..b27b443994 100644 --- a/internal/query/projection/session_test.go +++ b/internal/query/projection/session_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" @@ -30,7 +32,15 @@ func TestSessionProjection_reduces(t *testing.T) { session.AddedType, session.AggregateType, []byte(`{ - "domain": "domain" + "domain": "domain", + "user_agent": { + "fingerprint_id": "fp1", + "ip": "1.2.3.4", + "description": "firefox", + "header": { + "foo": ["bar"] + } + } }`), ), session.AddedEventMapper), }, @@ -41,7 +51,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.sessions5 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + expectedStmt: "INSERT INTO projections.sessions6 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator, user_agent_fingerprint_id, user_agent_description, user_agent_ip, user_agent_header) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -51,6 +61,10 @@ func TestSessionProjection_reduces(t *testing.T) { domain.SessionStateActive, uint64(15), "editor-user", + gu.Ptr("fp1"), + gu.Ptr("firefox"), + "1.2.3.4", + []byte(`{"foo":["bar"]}`), }, }, }, @@ -76,7 +90,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -108,7 +122,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -140,7 +154,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -172,7 +186,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -203,7 +217,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -234,7 +248,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -267,7 +281,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -298,7 +312,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions5 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.sessions6 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -325,7 +339,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions5 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.sessions6 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -355,7 +369,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.sessions5 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)", + expectedStmt: "UPDATE projections.sessions6 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)", expectedArgs: []interface{}{ nil, "agg-id", diff --git a/internal/query/session.go b/internal/query/session.go index bcc87a3031..c2921d235d 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" errs "errors" + "net" + "net/http" "time" sq "github.com/Masterminds/squirrel" @@ -41,6 +43,7 @@ type Session struct { OTPSMSFactor SessionOTPFactor OTPEmailFactor SessionOTPFactor Metadata map[string][]byte + UserAgent domain.UserAgent } type SessionUserFactor struct { @@ -166,6 +169,22 @@ var ( name: projection.SessionColumnTokenID, table: sessionsTable, } + SessionColumnUserAgentFingerprintID = Column{ + name: projection.SessionColumnUserAgentFingerprintID, + table: sessionsTable, + } + SessionColumnUserAgentIP = Column{ + name: projection.SessionColumnUserAgentIP, + table: sessionsTable, + } + SessionColumnUserAgentDescription = Column{ + name: projection.SessionColumnUserAgentDescription, + table: sessionsTable, + } + SessionColumnUserAgentHeader = Column{ + name: projection.SessionColumnUserAgentHeader, + table: sessionsTable, + } ) func (q *Queries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (session *Session, err error) { @@ -265,6 +284,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil SessionColumnOTPEmailCheckedAt.identifier(), SessionColumnMetadata.identifier(), SessionColumnToken.identifier(), + SessionColumnUserAgentFingerprintID.identifier(), + SessionColumnUserAgentIP.identifier(), + SessionColumnUserAgentDescription.identifier(), + SessionColumnUserAgentHeader.identifier(), ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). @@ -287,6 +310,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil otpEmailCheckedAt sql.NullTime metadata database.Map[[]byte] token sql.NullString + userAgentIP sql.NullString + userAgentHeader database.Map[[]string] ) err := row.Scan( @@ -311,6 +336,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil &otpEmailCheckedAt, &metadata, &token, + &session.UserAgent.FingerprintID, + &userAgentIP, + &session.UserAgent.Description, + &userAgentHeader, ) if err != nil { @@ -333,7 +362,11 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time session.Metadata = metadata + session.UserAgent.Header = http.Header(userAgentHeader) + if userAgentIP.Valid { + session.UserAgent.IP = net.ParseIP(userAgentIP.String) + } return session, token.String, nil } } diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index fa5209bdd3..1bae095ec6 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -6,10 +6,13 @@ import ( "database/sql/driver" "errors" "fmt" + "net" + "net/http" "regexp" "testing" sq "github.com/Masterminds/squirrel" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/domain" @@ -17,57 +20,61 @@ import ( ) var ( - expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions5.id,` + - ` projections.sessions5.creation_date,` + - ` projections.sessions5.change_date,` + - ` projections.sessions5.sequence,` + - ` projections.sessions5.state,` + - ` projections.sessions5.resource_owner,` + - ` projections.sessions5.creator,` + - ` projections.sessions5.user_id,` + - ` projections.sessions5.user_checked_at,` + + expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + + ` projections.sessions6.creation_date,` + + ` projections.sessions6.change_date,` + + ` projections.sessions6.sequence,` + + ` projections.sessions6.state,` + + ` projections.sessions6.resource_owner,` + + ` projections.sessions6.creator,` + + ` projections.sessions6.user_id,` + + ` projections.sessions6.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + ` projections.users8.resource_owner,` + - ` projections.sessions5.password_checked_at,` + - ` projections.sessions5.intent_checked_at,` + - ` projections.sessions5.webauthn_checked_at,` + - ` projections.sessions5.webauthn_user_verified,` + - ` projections.sessions5.totp_checked_at,` + - ` projections.sessions5.otp_sms_checked_at,` + - ` projections.sessions5.otp_email_checked_at,` + - ` projections.sessions5.metadata,` + - ` projections.sessions5.token_id` + - ` FROM projections.sessions5` + - ` LEFT JOIN projections.login_names2 ON projections.sessions5.user_id = projections.login_names2.user_id AND projections.sessions5.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions5.user_id = projections.users8_humans.user_id AND projections.sessions5.instance_id = projections.users8_humans.instance_id` + - ` LEFT JOIN projections.users8 ON projections.sessions5.user_id = projections.users8.id AND projections.sessions5.instance_id = projections.users8.instance_id` + + ` projections.sessions6.password_checked_at,` + + ` projections.sessions6.intent_checked_at,` + + ` projections.sessions6.webauthn_checked_at,` + + ` projections.sessions6.webauthn_user_verified,` + + ` projections.sessions6.totp_checked_at,` + + ` projections.sessions6.otp_sms_checked_at,` + + ` projections.sessions6.otp_email_checked_at,` + + ` projections.sessions6.metadata,` + + ` projections.sessions6.token_id,` + + ` projections.sessions6.user_agent_fingerprint_id,` + + ` projections.sessions6.user_agent_ip,` + + ` projections.sessions6.user_agent_description,` + + ` projections.sessions6.user_agent_header` + + ` FROM projections.sessions6` + + ` LEFT JOIN projections.login_names2 ON projections.sessions6.user_id = projections.login_names2.user_id AND projections.sessions6.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions6.user_id = projections.users8_humans.user_id AND projections.sessions6.instance_id = projections.users8_humans.instance_id` + + ` LEFT JOIN projections.users8 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) - expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions5.id,` + - ` projections.sessions5.creation_date,` + - ` projections.sessions5.change_date,` + - ` projections.sessions5.sequence,` + - ` projections.sessions5.state,` + - ` projections.sessions5.resource_owner,` + - ` projections.sessions5.creator,` + - ` projections.sessions5.user_id,` + - ` projections.sessions5.user_checked_at,` + + expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + + ` projections.sessions6.creation_date,` + + ` projections.sessions6.change_date,` + + ` projections.sessions6.sequence,` + + ` projections.sessions6.state,` + + ` projections.sessions6.resource_owner,` + + ` projections.sessions6.creator,` + + ` projections.sessions6.user_id,` + + ` projections.sessions6.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + ` projections.users8.resource_owner,` + - ` projections.sessions5.password_checked_at,` + - ` projections.sessions5.intent_checked_at,` + - ` projections.sessions5.webauthn_checked_at,` + - ` projections.sessions5.webauthn_user_verified,` + - ` projections.sessions5.totp_checked_at,` + - ` projections.sessions5.otp_sms_checked_at,` + - ` projections.sessions5.otp_email_checked_at,` + - ` projections.sessions5.metadata,` + + ` projections.sessions6.password_checked_at,` + + ` projections.sessions6.intent_checked_at,` + + ` projections.sessions6.webauthn_checked_at,` + + ` projections.sessions6.webauthn_user_verified,` + + ` projections.sessions6.totp_checked_at,` + + ` projections.sessions6.otp_sms_checked_at,` + + ` projections.sessions6.otp_email_checked_at,` + + ` projections.sessions6.metadata,` + ` COUNT(*) OVER ()` + - ` FROM projections.sessions5` + - ` LEFT JOIN projections.login_names2 ON projections.sessions5.user_id = projections.login_names2.user_id AND projections.sessions5.instance_id = projections.login_names2.instance_id` + - ` LEFT JOIN projections.users8_humans ON projections.sessions5.user_id = projections.users8_humans.user_id AND projections.sessions5.instance_id = projections.users8_humans.instance_id` + - ` LEFT JOIN projections.users8 ON projections.sessions5.user_id = projections.users8.id AND projections.sessions5.instance_id = projections.users8.instance_id` + + ` FROM projections.sessions6` + + ` LEFT JOIN projections.login_names2 ON projections.sessions6.user_id = projections.login_names2.user_id AND projections.sessions6.instance_id = projections.login_names2.instance_id` + + ` LEFT JOIN projections.users8_humans ON projections.sessions6.user_id = projections.users8_humans.user_id AND projections.sessions6.instance_id = projections.users8_humans.instance_id` + + ` LEFT JOIN projections.users8 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) sessionCols = []string{ @@ -92,6 +99,10 @@ var ( "otp_email_checked_at", "metadata", "token", + "user_agent_fingerprint_id", + "user_agent_ip", + "user_agent_description", + "user_agent_header", } sessionsCols = []string{ @@ -443,6 +454,10 @@ func Test_SessionPrepare(t *testing.T) { testNow, []byte(`{"key": "dmFsdWU="}`), "tokenID", + "fingerPrintID", + "1.2.3.4", + "agentDescription", + []byte(`{"foo":["foo","bar"]}`), }, ), }, @@ -483,6 +498,12 @@ func Test_SessionPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + UserAgent: domain.UserAgent{ + FingerprintID: gu.Ptr("fingerPrintID"), + IP: net.IPv4(1, 2, 3, 4), + Description: gu.Ptr("agentDescription"), + Header: http.Header{"foo": []string{"foo", "bar"}}, + }, }, }, { diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index d7b8bd76b9..83d905ea8c 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -33,6 +33,7 @@ const ( type AddedEvent struct { eventstore.BaseEvent `json:"-"` + UserAgent *domain.UserAgent `json:"user_agent,omitempty"` } func (e *AddedEvent) Payload() interface{} { @@ -45,6 +46,7 @@ func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { func NewAddedEvent(ctx context.Context, aggregate *eventstore.Aggregate, + userAgent *domain.UserAgent, ) *AddedEvent { return &AddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -52,6 +54,7 @@ func NewAddedEvent(ctx context.Context, aggregate, AddedType, ), + UserAgent: userAgent, } } diff --git a/pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go b/pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go new file mode 100644 index 0000000000..fe2509bd95 --- /dev/null +++ b/pkg/grpc/user/v2beta/user_service_org.pb.zitadel.go @@ -0,0 +1,12 @@ +package user + +import "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" + +// OrganisationFromRequest implements deprecated [middleware.OrganisationFromRequest] interface. +// it will be removed before going GA (https://github.com/zitadel/zitadel/issues/6718) +func (r *AddHumanUserRequest) OrganisationFromRequest() *middleware.Organization { + return &middleware.Organization{ + ID: r.GetOrganisation().GetOrgId(), + Domain: r.GetOrganisation().GetOrgDomain(), + } +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 620936f12b..c1f32616b2 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -3763,7 +3763,7 @@ service AdminService { // Activates the "LoginDefaultOrg" feature by setting the flag to "true" // This is irreversible! - // Once activated, the login UI will use the settings of the default org (and not from the instance) if not organisation context is set + // Once activated, the login UI will use the settings of the default org (and not from the instance) if not organization context is set rpc ActivateFeatureLoginDefaultOrg(ActivateFeatureLoginDefaultOrgRequest) returns (ActivateFeatureLoginDefaultOrgResponse) { option (google.api.http) = { put: "/features/login_default_org" @@ -4887,7 +4887,7 @@ message AddGenericOAuthProviderRequest { } ]; string client_secret = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret generated by the identity provider"; @@ -4954,7 +4954,7 @@ message UpdateGenericOAuthProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 4 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret will only be updated if provided"; @@ -5025,7 +5025,7 @@ message AddGenericOIDCProviderRequest { } ]; string client_secret = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "secret generated by the identity provider" @@ -5076,7 +5076,7 @@ message UpdateGenericOIDCProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 5 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "client secret will only be updated if provided"; @@ -6837,7 +6837,7 @@ message SetDefaultVerifyEmailMessageTextRequest { string text = 6 [ (validate.rules).string = {max_bytes: 40000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" + example: "\"A new email has been added. Please use the button below to verify your email. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" max_length: 10000; } ]; diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 6770f77427..3c485f7a92 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -1255,7 +1255,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Remove Multi-Factor OTP"; - description: "Remove the configured One-Time-Password (OTP) as a factor from the user. OTP is an authentication app, like Authy or Google/Microsoft Authenticator.." + description: "Remove the configured One-Time-Password (OTP) as a factor from the user. OTP is an authentication app, like Authy or Google/Microsoft Authenticator." tags: "Users"; tags: "User Human"; responses: { @@ -1306,6 +1306,68 @@ service ManagementService { }; } + rpc RemoveHumanAuthFactorOTPSMS(RemoveHumanAuthFactorOTPSMSRequest) returns (RemoveHumanAuthFactorOTPSMSResponse) { + option (google.api.http) = { + delete: "/users/{user_id}/auth_factors/otp_sms" + }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Remove Multi-Factor OTP SMS"; + description: "Remove the configured One-Time-Password (OTP) SMS as a factor from the user. As only one OTP SMS per user is allowed, the user will not have OTP SMS as a second-factor afterward." + tags: "Users"; + tags: "User Human"; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get a user from another organization include the header. Make sure the requesting user has permission in the requested organization."; + type: STRING, + required: false; + }; + }; + }; + } + + rpc RemoveHumanAuthFactorOTPEmail(RemoveHumanAuthFactorOTPEmailRequest) returns (RemoveHumanAuthFactorOTPEmailResponse) { + option (google.api.http) = { + delete: "/users/{user_id}/auth_factors/otp_email" + }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Remove Multi-Factor OTP SMS"; + description: "Remove the configured One-Time-Password (OTP) Email as a factor from the user. As only one OTP Email per user is allowed, the user will not have OTP Email as a second-factor afterward." + tags: "Users"; + tags: "User Human"; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get a user from another organization include the header. Make sure the requesting user has permission in the requested organization."; + type: STRING, + required: false; + }; + }; + }; + } + rpc ListHumanPasswordless(ListHumanPasswordlessRequest) returns (ListHumanPasswordlessResponse) { option (google.api.http) = { post: "/users/{user_id}/passwordless/_search" @@ -6806,7 +6868,7 @@ service ManagementService { }; } - // Add a new Azure AD identity provider in the organisation + // Add a new Azure AD identity provider in the organization rpc AddAzureADProvider(AddAzureADProviderRequest) returns (AddAzureADProviderResponse) { option (google.api.http) = { post: "/idps/azure" @@ -6824,7 +6886,7 @@ service ManagementService { }; } - // Change an existing Azure AD identity provider in the organisation + // Change an existing Azure AD identity provider in the organization rpc UpdateAzureADProvider(UpdateAzureADProviderRequest) returns (UpdateAzureADProviderResponse) { option (google.api.http) = { put: "/idps/azure/{id}" @@ -8246,6 +8308,22 @@ message RemoveHumanAuthFactorU2FResponse { zitadel.v1.ObjectDetails details = 1; } +message RemoveHumanAuthFactorOTPSMSRequest { + string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message RemoveHumanAuthFactorOTPSMSResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message RemoveHumanAuthFactorOTPEmailRequest { + string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message RemoveHumanAuthFactorOTPEmailResponse { + zitadel.v1.ObjectDetails details = 1; +} + message ListHumanPasswordlessRequest { string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -10914,7 +10992,7 @@ message SetCustomVerifyEmailMessageTextRequest { string text = 6 [ (validate.rules).string = {max_bytes: 40000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" + example: "\"A new email has been added. Please use the button below to verify your email. (Code {{.Code}}) If you didn't add a new email, please ignore this email.\"" max_length: 10000; } ]; @@ -11759,7 +11837,7 @@ message AddGenericOAuthProviderRequest { } ]; string client_secret = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret generated by the identity provider"; @@ -11826,7 +11904,7 @@ message UpdateGenericOAuthProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 4 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"client-secret\""; description: "Client secret will only be updated if provided"; @@ -11897,7 +11975,7 @@ message AddGenericOIDCProviderRequest { } ]; string client_secret = 4 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "secret generated by the identity provider" @@ -11948,7 +12026,7 @@ message UpdateGenericOIDCProviderRequest { ]; // client_secret will only be updated if provided string client_secret = 5 [ - (validate.rules).string = {max_len: 200}, + (validate.rules).string = {max_len: 1000}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"secret\""; description: "client secret will only be updated if provided"; diff --git a/proto/zitadel/object/v2beta/object.proto b/proto/zitadel/object/v2beta/object.proto index 75f82d98ae..c7c68f31ce 100644 --- a/proto/zitadel/object/v2beta/object.proto +++ b/proto/zitadel/object/v2beta/object.proto @@ -8,6 +8,7 @@ import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +// Deprecated: use Organization message Organisation { oneof org { string org_id = 1; @@ -15,6 +16,13 @@ message Organisation { } } +message Organization { + oneof org { + string org_id = 1; + string org_domain = 2; + } +} + message RequestContext { oneof resource_owner { string org_id = 1; diff --git a/proto/zitadel/project.proto b/proto/zitadel/project.proto index e050ba3dfa..b487a2dd34 100644 --- a/proto/zitadel/project.proto +++ b/proto/zitadel/project.proto @@ -48,7 +48,7 @@ message GrantedProject { ]; string granted_org_name = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"Some Organisation\"" + example: "\"Some Organization\"" } ]; repeated string granted_role_keys = 4 [ diff --git a/proto/zitadel/session/v2beta/session.proto b/proto/zitadel/session/v2beta/session.proto index ddbe143361..08ef97e215 100644 --- a/proto/zitadel/session/v2beta/session.proto +++ b/proto/zitadel/session/v2beta/session.proto @@ -39,6 +39,7 @@ message Session { description: "\"custom key value list\""; } ]; + UserAgent user_agent = 7; } message Factors { @@ -72,9 +73,15 @@ message UserFactor { description: "\"display name of the checked user\""; } ]; + // deprecated: use organization_id, will be remove before GA (https://github.com/zitadel/zitadel/issues/6718) string organisation_id = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "\"organisation id of the checked user\""; + description: "\"organization id of the checked user; deprecated: use organization_id\""; + } + ]; + string organization_id = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "\"organization id of the checked user\""; } ]; } @@ -131,3 +138,18 @@ message SearchQuery { message IDsQuery { repeated string ids = 1; } + +message UserAgent { + optional string fingerprint_id = 1; + optional string ip = 2; + optional string description = 3; + + // A header may have multiple values. + // In Go, headers are defined + // as map[string][]string, but protobuf + // doesn't allow this scheme. + message HeaderValues { + repeated string values = 1; + } + map header = 4; +} \ No newline at end of file diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index 745c430019..718a29381f 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -274,6 +274,7 @@ message CreateSessionRequest{ } ]; RequestChallenges challenges = 3; + UserAgent user_agent = 4; } message CreateSessionResponse{ diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 01abeceed4..36517dc4c5 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -119,7 +119,7 @@ service UserService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { permission: "user.write" - org_field: "organisation" + org_field: "organization" } http_response: { success_code: 201 @@ -655,7 +655,13 @@ message AddHumanUserRequest{ example: "\"minnie-mouse\""; } ]; - zitadel.object.v2beta.Organisation organisation = 3; + // deprecated: use organization (if both are set, organization will take precedence) + zitadel.object.v2beta.Organisation organisation = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "deprecated: use organization (if both are set, organization will take precedence)" + } + ]; + zitadel.object.v2beta.Organization organization = 11; SetHumanProfile profile = 4 [ (validate.rules).message.required = true, (google.api.field_behavior) = REQUIRED From 3a01558c61cbc5e86767217deeb11e727f05da43 Mon Sep 17 00:00:00 2001 From: Silvan Date: Thu, 19 Oct 2023 15:13:50 +0200 Subject: [PATCH 17/48] docs: add technical advisory 06 (#6756) --- docs/docs/support/advisory/a10006.md | 25 ++++++++++++++++++++++++ docs/docs/support/technical_advisory.mdx | 12 ++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 docs/docs/support/advisory/a10006.md diff --git a/docs/docs/support/advisory/a10006.md b/docs/docs/support/advisory/a10006.md new file mode 100644 index 0000000000..a0327c7538 --- /dev/null +++ b/docs/docs/support/advisory/a10006.md @@ -0,0 +1,25 @@ +--- +title: Technical Advisory 10005 +--- + +## Date and Version + +Version: 2.39.0 + +Date: Calendar week 41/42 2023 + +## Description + +Versions >= 2.39.0 require the cockroach database user of ZITADEL to be granted to the `VIEWACTIVITY` grant. This can either be reached by grant the role manually or execute the `zitadel init` command. + +## Statement + +To query correct order of events the cockroach database user of ZITADEL needs additional privileges to query the `crdb_internal.cluster_transactions`-table + +## Mitigation + +Before migrating to versions >= 2.39.0 make sure the cockroach database user has sufficient grants. + +## Impact + +If the user doesn't have sufficient grants, events won't be updated. diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index deb87fb3ac..a359b2ee83 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -118,6 +118,18 @@ We understand that these advisories may include breaking changes, and we aim to 2.39.0 Calendar week 41/42 2023 + + + A-10006 + + Additional grant to cockroach database user + Breaking Behaviour Change + + Versions >= 2.39.0 require the cockroach database user of ZITADEL to be granted to the `VIEWACTIVITY` grant. This can either be reached by grant the role manually or execute the `zitadel init` command. + + 2.39.0 + Calendar week 41/42 2023 + ## Subscribe to our Mailing List From 4d4f649edaa77696af647c469b3d3902737dacc4 Mon Sep 17 00:00:00 2001 From: Silvan Date: Thu, 19 Oct 2023 15:37:22 +0200 Subject: [PATCH 18/48] fix(db): allow unlimited connections (#6758) --- internal/database/dialect/connections.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/database/dialect/connections.go b/internal/database/dialect/connections.go index bf3a4680f3..cd6e87daa8 100644 --- a/internal/database/dialect/connections.go +++ b/internal/database/dialect/connections.go @@ -16,7 +16,7 @@ func NewConnectionInfo(openConns, idleConns uint32, pusherRatio float64) (*Conne if pusherRatio < 0 || pusherRatio > 1 { return nil, errors.New("EventPushConnRatio must be between 0 and 1") } - if openConns < 2 { + if openConns != 0 && openConns < 2 { return nil, errors.New("MaxOpenConns of the database must be higher that 1") } @@ -25,15 +25,19 @@ func NewConnectionInfo(openConns, idleConns uint32, pusherRatio float64) (*Conne info.EventstorePusher.MaxOpenConns = uint32(pusherRatio * float64(openConns)) info.EventstorePusher.MaxIdleConns = uint32(pusherRatio * float64(idleConns)) - if info.EventstorePusher.MaxOpenConns < 1 && pusherRatio > 0 { + if openConns != 0 && info.EventstorePusher.MaxOpenConns < 1 && pusherRatio > 0 { info.EventstorePusher.MaxOpenConns = 1 } - if info.EventstorePusher.MaxIdleConns < 1 && pusherRatio > 0 { + if idleConns != 0 && info.EventstorePusher.MaxIdleConns < 1 && pusherRatio > 0 { info.EventstorePusher.MaxIdleConns = 1 } - info.ZITADEL.MaxOpenConns = openConns - info.EventstorePusher.MaxOpenConns - info.ZITADEL.MaxIdleConns = idleConns - info.EventstorePusher.MaxIdleConns + if openConns != 0 { + info.ZITADEL.MaxOpenConns = openConns - info.EventstorePusher.MaxOpenConns + } + if idleConns != 0 { + info.ZITADEL.MaxIdleConns = idleConns - info.EventstorePusher.MaxIdleConns + } return info, nil } From 459761d99a7dfe70a054bc0dda65ae1405a06d36 Mon Sep 17 00:00:00 2001 From: Silvan Date: Thu, 19 Oct 2023 16:55:09 +0200 Subject: [PATCH 19/48] docs: correct title of tech advisory 06 (#6759) --- docs/docs/support/advisory/a10006.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/support/advisory/a10006.md b/docs/docs/support/advisory/a10006.md index a0327c7538..47042da4cb 100644 --- a/docs/docs/support/advisory/a10006.md +++ b/docs/docs/support/advisory/a10006.md @@ -1,5 +1,5 @@ --- -title: Technical Advisory 10005 +title: Technical Advisory 10006 --- ## Date and Version From ab79855cf059af55e27506b29dfec580751ee2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Thu, 19 Oct 2023 18:21:31 +0300 Subject: [PATCH 20/48] fix(eventstore): prevent allocation of filtered events (#6749) * fix(eventstore): prevent allocation of filtered events Directly reduce each event obtained from a sql.Rows scan, so that we do not have to allocate all events in a slice. * reinstate the mutex as RWMutex * scan data directly * add todos * fix(writemodels): add reduce of parent * test: remove comment * update comments --------- Co-authored-by: adlerhurst --- .../command/existing_label_policies_model.go | 2 +- internal/command/instance_domain_model.go | 2 +- internal/command/instance_model.go | 2 +- internal/command/org_domain_model.go | 6 +- internal/command/preparation/command.go | 2 + internal/command/quota_model.go | 2 +- internal/command/system_model.go | 2 +- internal/command/unique_constraints_model.go | 2 +- internal/eventstore/eventstore.go | 79 +++++++++++-------- internal/eventstore/eventstore_test.go | 18 +++-- .../repository/mock/repository.mock.go | 17 ++-- .../repository/mock/repository.mock.impl.go | 15 +++- internal/eventstore/repository/sql/crdb.go | 22 +++--- internal/eventstore/repository/sql/query.go | 17 ++-- .../eventstore/repository/sql/query_test.go | 46 ++++++++--- internal/eventstore/search_query.go | 9 +++ 16 files changed, 150 insertions(+), 93 deletions(-) diff --git a/internal/command/existing_label_policies_model.go b/internal/command/existing_label_policies_model.go index af48427740..dda39980f1 100644 --- a/internal/command/existing_label_policies_model.go +++ b/internal/command/existing_label_policies_model.go @@ -38,7 +38,7 @@ func (rm *ExistingLabelPoliciesReadModel) Reduce() error { } } } - return nil + return rm.WriteModel.Reduce() } func (rm *ExistingLabelPoliciesReadModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/instance_domain_model.go b/internal/command/instance_domain_model.go index 06af63f8b2..2126741a88 100644 --- a/internal/command/instance_domain_model.go +++ b/internal/command/instance_domain_model.go @@ -55,7 +55,7 @@ func (wm *InstanceDomainWriteModel) Reduce() error { wm.State = domain.InstanceDomainStateRemoved } } - return nil + return wm.WriteModel.Reduce() } func (wm *InstanceDomainWriteModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/instance_model.go b/internal/command/instance_model.go index 6db058aff9..98d49926bf 100644 --- a/internal/command/instance_model.go +++ b/internal/command/instance_model.go @@ -60,7 +60,7 @@ func (wm *InstanceWriteModel) Reduce() error { wm.DefaultLanguage = e.Language } } - return nil + return wm.WriteModel.Reduce() } func (wm *InstanceWriteModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/org_domain_model.go b/internal/command/org_domain_model.go index a23d740696..43ab9e52dd 100644 --- a/internal/command/org_domain_model.go +++ b/internal/command/org_domain_model.go @@ -84,7 +84,7 @@ func (wm *OrgDomainWriteModel) Reduce() error { wm.ValidationCode = nil } } - return nil + return wm.WriteModel.Reduce() } func (wm *OrgDomainWriteModel) Query() *eventstore.SearchQueryBuilder { @@ -154,7 +154,7 @@ func (wm *OrgDomainsWriteModel) Reduce() error { } } } - return nil + return wm.WriteModel.Reduce() } func (wm *OrgDomainsWriteModel) Query() *eventstore.SearchQueryBuilder { @@ -216,7 +216,7 @@ func (wm *OrgDomainVerifiedWriteModel) Reduce() error { wm.Verified = false } } - return nil + return wm.WriteModel.Reduce() } func (wm *OrgDomainVerifiedWriteModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/preparation/command.go b/internal/command/preparation/command.go index 3d0ac69925..96a3da8909 100644 --- a/internal/command/preparation/command.go +++ b/internal/command/preparation/command.go @@ -25,6 +25,8 @@ var ( ) // PrepareCommands checks the passed validations and if ok creates the commands +// +// Deprecated: filter causes unneeded allocation. Use [eventstore.FilterToQueryReducer] instead. func PrepareCommands(ctx context.Context, filter FilterToQueryReducer, validations ...Validation) (cmds []eventstore.Command, err error) { commanders, err := validate(validations) if err != nil { diff --git a/internal/command/quota_model.go b/internal/command/quota_model.go index 3c4c87c401..8c2efa7dfe 100644 --- a/internal/command/quota_model.go +++ b/internal/command/quota_model.go @@ -79,7 +79,7 @@ func (wm *quotaWriteModel) Reduce() error { } // wm.WriteModel.Reduce() sets the aggregateID to the first event's aggregateID, but we need the last one wm.AggregateID = wm.rollingAggregateID - return nil + return wm.WriteModel.Reduce() } // NewChanges returns all changes that need to be applied to the aggregate. diff --git a/internal/command/system_model.go b/internal/command/system_model.go index 6637013d55..4021e5e46d 100644 --- a/internal/command/system_model.go +++ b/internal/command/system_model.go @@ -94,7 +94,7 @@ func (wm *SystemConfigWriteModel) Reduce() error { } } } - return nil + return wm.WriteModel.Reduce() } func (wm *SystemConfigWriteModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/command/unique_constraints_model.go b/internal/command/unique_constraints_model.go index c09f6343c2..9b71b0e7a1 100644 --- a/internal/command/unique_constraints_model.go +++ b/internal/command/unique_constraints_model.go @@ -183,7 +183,7 @@ func (rm *UniqueConstraintReadModel) Reduce() error { rm.removeUniqueConstraint(e.Aggregate().ID, e.UserID, member.UniqueMember) } } - return nil + return rm.WriteModel.Reduce() } func (rm *UniqueConstraintReadModel) Query() *eventstore.SearchQueryBuilder { diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 2a8d8a02d7..0bf31afb48 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -12,7 +12,9 @@ import ( // Eventstore abstracts all functions needed to store valid events // and filters the stored events type Eventstore struct { - interceptorMutex sync.Mutex + // TODO: get rid of this mutex, + // or if we scale to >4vCPU use a sync.Map + interceptorMutex sync.RWMutex eventInterceptors map[EventType]eventTypeInterceptors eventTypes []string aggregateTypes []string @@ -33,7 +35,6 @@ type eventTypeInterceptors struct { func NewEventstore(config *Config) *Eventstore { return &Eventstore{ eventInterceptors: map[EventType]eventTypeInterceptors{}, - interceptorMutex: sync.Mutex{}, PushTimeout: config.PushTimeout, pusher: config.Pusher, @@ -83,28 +84,33 @@ func (es *Eventstore) AggregateTypes() []string { // Filter filters the stored events based on the searchQuery // and maps the events to the defined event structs -func (es *Eventstore) Filter(ctx context.Context, queryFactory *SearchQueryBuilder) ([]Event, error) { - // make sure that the instance id is always set - if queryFactory.instanceID == nil && authz.GetInstance(ctx).InstanceID() != "" { - queryFactory.InstanceID(authz.GetInstance(ctx).InstanceID()) - } - - events, err := es.querier.Filter(ctx, queryFactory) +// +// Deprecated: Use [FilterToQueryReducer] instead to avoid allocations. +func (es *Eventstore) Filter(ctx context.Context, searchQuery *SearchQueryBuilder) ([]Event, error) { + events := make([]Event, 0, searchQuery.GetLimit()) + searchQuery.ensureInstanceID(ctx) + err := es.querier.FilterToReducer(ctx, searchQuery, func(event Event) error { + event, err := es.mapEvent(event) + if err != nil { + return err + } + events = append(events, event) + return nil + }) if err != nil { return nil, err } - - return es.mapEvents(events) + return events, nil } func (es *Eventstore) mapEvents(events []Event) (mappedEvents []Event, err error) { mappedEvents = make([]Event, len(events)) - es.interceptorMutex.Lock() - defer es.interceptorMutex.Unlock() + es.interceptorMutex.RLock() + defer es.interceptorMutex.RUnlock() for i, event := range events { - mappedEvents[i], err = es.mapEvent(event) + mappedEvents[i], err = es.mapEventLocked(event) if err != nil { return nil, err } @@ -114,6 +120,12 @@ func (es *Eventstore) mapEvents(events []Event) (mappedEvents []Event, err error } func (es *Eventstore) mapEvent(event Event) (Event, error) { + es.interceptorMutex.RLock() + defer es.interceptorMutex.RUnlock() + return es.mapEventLocked(event) +} + +func (es *Eventstore) mapEventLocked(event Event) (Event, error) { interceptors, ok := es.eventInterceptors[event.Type()] if !ok || interceptors.eventMapper == nil { return BaseEventFromRepo(event), nil @@ -121,6 +133,14 @@ func (es *Eventstore) mapEvent(event Event) (Event, error) { return interceptors.eventMapper(event) } +// TODO: refactor so we can change to the following interface: +/* +type reducer interface { + // Reduce applies an event on the object. + Reduce(Event) error +} +*/ + type reducer interface { //Reduce handles the events of the internal events list // it only appends the newly added events @@ -131,14 +151,15 @@ type reducer interface { // FilterToReducer filters the events based on the search query, appends all events to the reducer and calls it's reduce function func (es *Eventstore) FilterToReducer(ctx context.Context, searchQuery *SearchQueryBuilder, r reducer) error { - events, err := es.Filter(ctx, searchQuery) - if err != nil { - return err - } - - r.AppendEvents(events...) - - return r.Reduce() + searchQuery.ensureInstanceID(ctx) + return es.querier.FilterToReducer(ctx, searchQuery, func(event Event) error { + event, err := es.mapEvent(event) + if err != nil { + return err + } + r.AppendEvents(event) + return r.Reduce() + }) } // LatestSequence filters the latest sequence for the given search query @@ -180,13 +201,7 @@ type QueryReducer interface { // FilterToQueryReducer filters the events based on the search query of the query function, // appends all events to the reducer and calls it's reduce function func (es *Eventstore) FilterToQueryReducer(ctx context.Context, r QueryReducer) error { - events, err := es.Filter(ctx, r.Query()) - if err != nil { - return err - } - r.AppendEvents(events...) - - return r.Reduce() + return es.FilterToReducer(ctx, r.Query(), r) } // RegisterFilterEventMapper registers a function for mapping an eventstore event to an event @@ -207,11 +222,13 @@ func (es *Eventstore) RegisterFilterEventMapper(aggregateType AggregateType, eve return es } +type Reducer func(event Event) error + type Querier interface { // Health checks if the connection to the storage is available Health(ctx context.Context) error - // Filter returns all events matching the given search query - Filter(ctx context.Context, searchQuery *SearchQueryBuilder) (events []Event, err error) + // FilterToReducer calls r for every event returned from the storage + FilterToReducer(ctx context.Context, searchQuery *SearchQueryBuilder, r Reducer) error // LatestSequence returns the latest sequence found by the search query LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) // InstanceIDs returns the instance ids found by the search query diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index 8738a47afc..8fe9fd8592 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "reflect" - "sync" "testing" "time" @@ -402,6 +401,7 @@ func (repo *testQuerier) Health(ctx context.Context) error { func (repo *testQuerier) CreateInstance(ctx context.Context, instance string) error { return nil } + func (repo *testQuerier) Filter(ctx context.Context, searchQuery *SearchQueryBuilder) ([]Event, error) { if repo.err != nil { return nil, repo.err @@ -409,6 +409,18 @@ func (repo *testQuerier) Filter(ctx context.Context, searchQuery *SearchQueryBui return repo.events, nil } +func (repo *testQuerier) FilterToReducer(ctx context.Context, searchQuery *SearchQueryBuilder, reduce Reducer) error { + if repo.err != nil { + return repo.err + } + for _, event := range repo.events { + if err := reduce(event); err != nil { + return err + } + } + return nil +} + func (repo *testQuerier) LatestSequence(ctx context.Context, queryFactory *SearchQueryBuilder) (float64, error) { if repo.err != nil { return 0, repo.err @@ -684,7 +696,6 @@ func TestEventstore_Push(t *testing.T) { t.Run(tt.name, func(t *testing.T) { es := &Eventstore{ pusher: tt.fields.pusher, - interceptorMutex: sync.Mutex{}, eventInterceptors: map[EventType]eventTypeInterceptors{}, } for eventType, mapper := range tt.fields.eventMapper { @@ -816,7 +827,6 @@ func TestEventstore_FilterEvents(t *testing.T) { t.Run(tt.name, func(t *testing.T) { es := &Eventstore{ querier: tt.fields.repo, - interceptorMutex: sync.Mutex{}, eventInterceptors: map[EventType]eventTypeInterceptors{}, } @@ -1121,7 +1131,6 @@ func TestEventstore_FilterToReducer(t *testing.T) { t.Run(tt.name, func(t *testing.T) { es := &Eventstore{ querier: tt.fields.repo, - interceptorMutex: sync.Mutex{}, eventInterceptors: map[EventType]eventTypeInterceptors{}, } for eventType, mapper := range tt.fields.eventMapper { @@ -1238,7 +1247,6 @@ func TestEventstore_mapEvents(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { es := &Eventstore{ - interceptorMutex: sync.Mutex{}, eventInterceptors: map[EventType]eventTypeInterceptors{}, } for eventType, mapper := range tt.fields.eventMapper { diff --git a/internal/eventstore/repository/mock/repository.mock.go b/internal/eventstore/repository/mock/repository.mock.go index f2b4fb089a..67007e69ec 100644 --- a/internal/eventstore/repository/mock/repository.mock.go +++ b/internal/eventstore/repository/mock/repository.mock.go @@ -35,19 +35,18 @@ func (m *MockQuerier) EXPECT() *MockQuerierMockRecorder { return m.recorder } -// Filter mocks base method. -func (m *MockQuerier) Filter(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { +// FilterToReducer mocks base method. +func (m *MockQuerier) FilterToReducer(arg0 context.Context, arg1 *eventstore.SearchQueryBuilder, arg2 eventstore.Reducer) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Filter", arg0, arg1) - ret0, _ := ret[0].([]eventstore.Event) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "FilterToReducer", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 } -// Filter indicates an expected call of Filter. -func (mr *MockQuerierMockRecorder) Filter(arg0, arg1 interface{}) *gomock.Call { +// FilterToReducer indicates an expected call of FilterToReducer. +func (mr *MockQuerierMockRecorder) FilterToReducer(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filter", reflect.TypeOf((*MockQuerier)(nil).Filter), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterToReducer", reflect.TypeOf((*MockQuerier)(nil).FilterToReducer), arg0, arg1, arg2) } // Health mocks base method. diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go index b6d5eabbd8..1df54f8c56 100644 --- a/internal/eventstore/repository/mock/repository.mock.impl.go +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -30,21 +30,30 @@ func NewRepo(t *testing.T) *MockRepository { func (m *MockRepository) ExpectFilterNoEventsNoError() *MockRepository { m.MockQuerier.ctrl.T.Helper() - m.MockQuerier.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(nil, nil) + m.MockQuerier.EXPECT().FilterToReducer(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) return m } func (m *MockRepository) ExpectFilterEvents(events ...eventstore.Event) *MockRepository { m.MockQuerier.ctrl.T.Helper() - m.MockQuerier.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(events, nil) + m.MockQuerier.EXPECT().FilterToReducer(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, _ *eventstore.SearchQueryBuilder, reduce eventstore.Reducer) error { + for _, event := range events { + if err := reduce(event); err != nil { + return err + } + } + return nil + }, + ) return m } func (m *MockRepository) ExpectFilterEventsError(err error) *MockRepository { m.MockQuerier.ctrl.T.Helper() - m.MockQuerier.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(nil, err) + m.MockQuerier.EXPECT().FilterToReducer(gomock.Any(), gomock.Any(), gomock.Any()).Return(err) return m } diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go index 6d0cda612c..2d92e45ef7 100644 --- a/internal/eventstore/repository/sql/crdb.go +++ b/internal/eventstore/repository/sql/crdb.go @@ -247,22 +247,18 @@ func (db *CRDB) handleUniqueConstraints(ctx context.Context, tx *sql.Tx, uniqueC return nil } -// Filter returns all events matching the given search query -func (crdb *CRDB) Filter(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder) (events []eventstore.Event, err error) { - events = make([]eventstore.Event, 0, searchQuery.GetLimit()) - err = query(ctx, crdb, searchQuery, &events, false) +// FilterToReducer finds all events matching the given search query and passes them to the reduce function. +func (crdb *CRDB) FilterToReducer(ctx context.Context, searchQuery *eventstore.SearchQueryBuilder, reduce eventstore.Reducer) error { + err := query(ctx, crdb, searchQuery, reduce, false) + if err == nil { + return nil + } pgErr := new(pgconn.PgError) // check events2 not exists - if err != nil && errors.As(err, &pgErr) { - if pgErr.Code == "42P01" { - err = query(ctx, crdb, searchQuery, &events, true) - } + if errors.As(err, &pgErr) && pgErr.Code == "42P01" { + return query(ctx, crdb, searchQuery, reduce, true) } - if err != nil { - return nil, err - } - - return events, nil + return err } // LatestSequence returns the latest sequence found by the search query diff --git a/internal/eventstore/repository/sql/query.go b/internal/eventstore/repository/sql/query.go index 144a414fd1..aab8abbcfe 100644 --- a/internal/eventstore/repository/sql/query.go +++ b/internal/eventstore/repository/sql/query.go @@ -168,12 +168,11 @@ func instanceIDsScanner(scanner scan, dest interface{}) (err error) { func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) { return func(scanner scan, dest interface{}) (err error) { - events, ok := dest.(*[]eventstore.Event) + reduce, ok := dest.(eventstore.Reducer) if !ok { - return z_errors.ThrowInvalidArgument(nil, "SQL-4GP6F", "type must be event") + return z_errors.ThrowInvalidArgumentf(nil, "SQL-4GP6F", "events scanner: invalid type %T", dest) } event := new(repository.Event) - data := sql.RawBytes{} position := new(sql.NullFloat64) if useV1 { @@ -181,7 +180,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) &event.CreationDate, &event.Typ, &event.Seq, - &data, + &event.Data, &event.EditorUser, &event.ResourceOwner, &event.InstanceID, @@ -196,7 +195,7 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) &event.Typ, &event.Seq, position, - &data, + &event.Data, &event.EditorUser, &event.ResourceOwner, &event.InstanceID, @@ -211,14 +210,8 @@ func eventsScanner(useV1 bool) func(scanner scan, dest interface{}) (err error) logging.New().WithError(err).Warn("unable to scan row") return z_errors.ThrowInternal(err, "SQL-M0dsf", "unable to scan row") } - - event.Data = make([]byte, len(data)) - copy(event.Data, data) event.Pos = position.Float64 - - *events = append(*events, event) - - return nil + return reduce(event) } } diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 478191ec1e..8d6f670384 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database/cockroach" @@ -74,6 +75,8 @@ func Test_getCondition(t *testing.T) { } func Test_prepareColumns(t *testing.T) { + var reducedEvents []eventstore.Event + type fields struct { dbRow []interface{} } @@ -146,13 +149,16 @@ func Test_prepareColumns(t *testing.T) { name: "events", args: args{ columns: eventstore.ColumnsEvent, - dest: &[]eventstore.Event{}, - useV1: true, + dest: eventstore.Reducer(func(event eventstore.Event) error { + reducedEvents = append(reducedEvents, event) + return nil + }), + useV1: true, }, res: res{ query: `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Data: make(sql.RawBytes, 0)}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Data: nil}, }, }, fields: fields{ @@ -163,12 +169,15 @@ func Test_prepareColumns(t *testing.T) { name: "events v2", args: args{ columns: eventstore.ColumnsEvent, - dest: &[]eventstore.Event{}, + dest: eventstore.Reducer(func(event eventstore.Event) error { + reducedEvents = append(reducedEvents, event) + return nil + }), }, res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 42, Data: make(sql.RawBytes, 0), Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 42, Data: nil, Version: "v1"}, }, }, fields: fields{ @@ -179,12 +188,15 @@ func Test_prepareColumns(t *testing.T) { name: "event null position", args: args{ columns: eventstore.ColumnsEvent, - dest: &[]eventstore.Event{}, + dest: eventstore.Reducer(func(event eventstore.Event) error { + reducedEvents = append(reducedEvents, event) + return nil + }), }, res: res{ query: `SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2`, expected: []eventstore.Event{ - &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 0, Data: make(sql.RawBytes, 0), Version: "v1"}, + &repository.Event{AggregateID: "hodor", AggregateType: "user", Seq: 5, Pos: 0, Data: nil, Version: "v1"}, }, }, fields: fields{ @@ -207,9 +219,12 @@ func Test_prepareColumns(t *testing.T) { name: "event query error", args: args{ columns: eventstore.ColumnsEvent, - dest: &[]eventstore.Event{}, - dbErr: sql.ErrConnDone, - useV1: true, + dest: eventstore.Reducer(func(event eventstore.Event) error { + reducedEvents = append(reducedEvents, event) + return nil + }), + dbErr: sql.ErrConnDone, + useV1: true, }, res: res{ query: `SELECT creation_date, event_type, event_sequence, event_data, editor_user, resource_owner, instance_id, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events`, @@ -242,6 +257,12 @@ func Test_prepareColumns(t *testing.T) { equalizer.Equal(tt.args.dest.(*sql.NullTime).Time) return } + if _, ok := tt.args.dest.(eventstore.Reducer); ok { + assert.Equal(t, tt.res.expected, reducedEvents) + reducedEvents = nil + return + } + got := reflect.Indirect(reflect.ValueOf(tt.args.dest)).Interface() if !reflect.DeepEqual(got, tt.res.expected) { t.Errorf("unexpected result from rowScanner \nwant: %+v \ngot: %+v", tt.res.expected, got) @@ -625,7 +646,10 @@ func Test_query_events_with_crdb(t *testing.T) { } events := []eventstore.Event{} - if err := query(context.Background(), db, tt.args.searchQuery, &events, true); (err != nil) != tt.wantErr { + if err := query(context.Background(), db, tt.args.searchQuery, eventstore.Reducer(func(event eventstore.Event) error { + events = append(events, event) + return nil + }), true); (err != nil) != tt.wantErr { t.Errorf("CRDB.query() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/eventstore/search_query.go b/internal/eventstore/search_query.go index 3cdd7f4d81..2b102db9fb 100644 --- a/internal/eventstore/search_query.go +++ b/internal/eventstore/search_query.go @@ -1,9 +1,11 @@ package eventstore import ( + "context" "database/sql" "time" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/errors" ) @@ -82,6 +84,13 @@ func (q SearchQueryBuilder) GetCreationDateAfter() time.Time { return q.creationDateAfter } +// ensureInstanceID makes sure that the instance id is always set +func (b *SearchQueryBuilder) ensureInstanceID(ctx context.Context) { + if b.instanceID == nil && authz.GetInstance(ctx).InstanceID() != "" { + b.InstanceID(authz.GetInstance(ctx).InstanceID()) + } +} + type SearchQuery struct { builder *SearchQueryBuilder aggregateTypes []AggregateType From b4fd5667461ef741215fc94e7032ed769b3a3238 Mon Sep 17 00:00:00 2001 From: Miguel Cabrerizo <30386061+doncicuto@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:26:24 +0200 Subject: [PATCH 21/48] fix: missing ngOnInit fetch data (#6730) * fix: missing ngoninit fetch data * fix: e2e test for sms check setting has been added --------- Co-authored-by: Elio Bischof --- .../notification-sms-provider.component.ts | 7 +++++-- e2e/cypress/e2e/instance/settings/notifications.cy.ts | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts index eb14ce8a0c..f13a3e6992 100644 --- a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts +++ b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { AddSMSProviderTwilioRequest, UpdateSMSProviderTwilioRequest } from 'src/app/proto/generated/zitadel/admin_pb'; import { SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb'; @@ -15,7 +15,7 @@ import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog- templateUrl: './notification-sms-provider.component.html', styleUrls: ['./notification-sms-provider.component.scss'], }) -export class NotificationSMSProviderComponent { +export class NotificationSMSProviderComponent implements OnInit { @Input() public serviceType!: PolicyComponentServiceType; public smsProviders: SMSProvider.AsObject[] = []; @@ -30,6 +30,9 @@ export class NotificationSMSProviderComponent { private toast: ToastService, ) {} + ngOnInit(): void { + this.fetchData(); + } private fetchData(): void { this.smsProvidersLoading = true; this.service diff --git a/e2e/cypress/e2e/instance/settings/notifications.cy.ts b/e2e/cypress/e2e/instance/settings/notifications.cy.ts index 02548d6636..f2685c5d92 100644 --- a/e2e/cypress/e2e/instance/settings/notifications.cy.ts +++ b/e2e/cypress/e2e/instance/settings/notifications.cy.ts @@ -57,6 +57,8 @@ describe('instance notifications', () => { cy.get('[formcontrolname="senderNumber"]').clear().type('2312123132'); cy.get('[data-e2e="save-sms-settings-button"]').click(); cy.shouldConfirmSuccess(); + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Inactive'); }); }); }); From 36eeae10712561cdbd70edce8f4aba1eedd2070c Mon Sep 17 00:00:00 2001 From: Miguel Cabrerizo <30386061+doncicuto@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:55:39 +0200 Subject: [PATCH 22/48] fix(console): update Twilio sms provider settings (#6732) fix: update sms provider settings Co-authored-by: Elio Bischof --- .../dialog-add-sms-provider.component.html | 2 +- .../dialog-add-sms-provider.component.ts | 5 +- .../notification-sms-provider.component.html | 1 + .../notification-sms-provider.component.ts | 3 +- console/src/assets/i18n/bg.json | 1 + console/src/assets/i18n/de.json | 1 + console/src/assets/i18n/en.json | 1 + console/src/assets/i18n/es.json | 1 + console/src/assets/i18n/fr.json | 1 + console/src/assets/i18n/it.json | 1 + console/src/assets/i18n/ja.json | 1 + console/src/assets/i18n/mk.json | 1 + console/src/assets/i18n/pl.json | 1 + console/src/assets/i18n/pt.json | 1 + console/src/assets/i18n/zh.json | 1 + .../e2e/instance/settings/notifications.cy.ts | 53 ++++++++++++++++++- 16 files changed, 68 insertions(+), 7 deletions(-) diff --git a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html index b8d239ef18..78f70c7fbe 100644 --- a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html +++ b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.html @@ -18,7 +18,7 @@ - diff --git a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts index 2594dd0728..01c1d6890f 100644 --- a/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts +++ b/console/src/app/modules/policies/notification-sms-provider/dialog-add-sms-provider/dialog-add-sms-provider.component.ts @@ -61,15 +61,14 @@ export class DialogAddSMSProviderComponent { } public closeDialogWithRequest(): void { - if (!!this.twilio) { + if (!!this.twilio && this.twilioProvider && this.twilioProvider.id) { this.req = new UpdateSMSProviderTwilioRequest(); - + this.req.setId(this.twilioProvider.id); this.req.setSid(this.sid?.value); this.req.setSenderNumber(this.senderNumber?.value); this.dialogRef.close(this.req); } else { this.req = new AddSMSProviderTwilioRequest(); - this.req.setSid(this.sid?.value); this.req.setToken(this.token?.value); this.req.setSenderNumber(this.senderNumber?.value); diff --git a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html index adde321c63..33a2521e57 100644 --- a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html +++ b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.html @@ -24,6 +24,7 @@ *ngIf="twilio && twilio.id" [disabled]="(['iam.write'] | hasRole | async) === false" mat-stroked-button + data-e2e="activate-sms-provider-button" (click)="toggleSMSProviderState(twilio.id)" > {{ diff --git a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts index f13a3e6992..dc8b2115d4 100644 --- a/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts +++ b/console/src/app/modules/policies/notification-sms-provider/notification-sms-provider.component.ts @@ -33,6 +33,7 @@ export class NotificationSMSProviderComponent implements OnInit { ngOnInit(): void { this.fetchData(); } + private fetchData(): void { this.smsProvidersLoading = true; this.service @@ -63,7 +64,7 @@ export class NotificationSMSProviderComponent implements OnInit { this.service .updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest) .then(() => { - this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true); + this.toast.showInfo('SETTING.SMS.TWILIO.UPDATED', true); this.fetchData(); }) .catch((error) => { diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 2bb72ca7d6..e9907811e0 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1085,6 +1085,7 @@ "TOKEN": "Токен", "SENDERNUMBER": "Номер на изпращача", "ADDED": "Twilio добави успешно.", + "UPDATED": "Twilio се актуализира успешно.", "REMOVED": "Twilio премахнат", "CHANGETOKEN": "Промяна на токена", "SETTOKEN": "Задаване на токен", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 9ae13c827c..fc3c9633a8 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1091,6 +1091,7 @@ "TOKEN": "Token", "SENDERNUMBER": "Sender Number", "ADDED": "Twilio erfolgreich hinzugefügt.", + "UPDATED": "Twilio wurde erfolgreich aktualisiert.", "REMOVED": "Twilio entfernt", "CHANGETOKEN": "Token ändern", "SETTOKEN": "Token setzen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 25ff13be26..5166c33f5e 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1092,6 +1092,7 @@ "TOKEN": "Token", "SENDERNUMBER": "Sender Number", "ADDED": "Twilio added successfully.", + "UPDATED": "Twilio updated successfully.", "REMOVED": "Twilio removed", "CHANGETOKEN": "Change Token", "SETTOKEN": "Set Token", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 34c0c6de46..d4faf3abf4 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1092,6 +1092,7 @@ "TOKEN": "Token", "SENDERNUMBER": "Número de emisor", "ADDED": "Twilio añadido con éxito.", + "UPDATED": "Twilio actualizado con éxito", "REMOVED": "Twilio eliminado", "CHANGETOKEN": "Cambiar token", "SETTOKEN": "Establecer token", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 373feb32a2..bc52d02dd7 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1091,6 +1091,7 @@ "TOKEN": "Jeton", "SENDERNUMBER": "Numéro d'expéditeur", "ADDED": "Twilio a été ajouté avec succès.", + "UPDATED": "Twilio a été mis à jour avec succès.", "REMOVED": "Twilio a été supprimé avec succès", "CHANGETOKEN": "Changer de Token", "SETTOKEN": "Définir le jeton", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index cae3b1cf13..635d42c301 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1091,6 +1091,7 @@ "TOKEN": "Token", "SENDERNUMBER": "Sender Number", "ADDED": "Twilio aggiunto con successo.", + "UPDATED": "Twilio aggiornato correttamente.", "REMOVED": "Twilio rimosso con successo.", "CHANGETOKEN": "Cambia Token", "SETTOKEN": "Cambia Token", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 651d00a263..3f6bd719a9 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1092,6 +1092,7 @@ "TOKEN": "トークン", "SENDERNUMBER": "送信者番号", "ADDED": "Twilioは正常に追加されました。", + "UPDATED": "Twilio が正常に更新されました。", "REMOVED": "Twilioが削除されました", "CHANGETOKEN": "トークンを変更する", "SETTOKEN": "トークンを設定する", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index cad724be75..40360136b2 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1093,6 +1093,7 @@ "TOKEN": "Токен", "SENDERNUMBER": "Број на испраќач", "ADDED": "Twilio e успешно додаден.", + "UPDATED": "Twilio се ажурираше успешно.", "REMOVED": "Twilio отстранет", "CHANGETOKEN": "Смени токен", "SETTOKEN": "Постави токен", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index a6ef90e0e8..c78c6eef10 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1091,6 +1091,7 @@ "TOKEN": "Token", "SENDERNUMBER": "Numer nadawcy", "ADDED": "Twilio dodano pomyślnie.", + "UPDATED": "Twilio zostało pomyślnie zaktualizowane.", "REMOVED": "Twilio usunięte", "CHANGETOKEN": "Zmień Token", "SETTOKEN": "Ustaw Token", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index fb03b6c71a..bd2a648a0a 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1093,6 +1093,7 @@ "TOKEN": "Token", "SENDERNUMBER": "Número do remetente", "ADDED": "Twilio adicionado com sucesso.", + "UPDATED": "Twilio atualizado com sucesso.", "REMOVED": "Twilio removido", "CHANGETOKEN": "Alterar token", "SETTOKEN": "Definir token", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 4192baed91..7d34e7a47f 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1091,6 +1091,7 @@ "TOKEN": "令牌", "SENDERNUMBER": "发件人号码", "ADDED": "Twilio 添加成功。", + "UPDATED": "Twilio 更新成功。", "REMOVED": "Twilio 已删除", "CHANGETOKEN": "更改令牌", "SETTOKEN": "设置令牌", diff --git a/e2e/cypress/e2e/instance/settings/notifications.cy.ts b/e2e/cypress/e2e/instance/settings/notifications.cy.ts index f2685c5d92..06207ce524 100644 --- a/e2e/cypress/e2e/instance/settings/notifications.cy.ts +++ b/e2e/cypress/e2e/instance/settings/notifications.cy.ts @@ -19,7 +19,6 @@ describe('instance notifications', () => { cy.visit(smtpPath); cy.contains('SMTP Settings'); }); - it(`should add SMTP provider settings`, () => { cy.visit(smtpPath); cy.get('[formcontrolname="senderAddress"]').clear().type('sender@example.com'); @@ -33,7 +32,6 @@ describe('instance notifications', () => { cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailtrap.io:2525'); cy.get('[formcontrolname="user"]').should('have.value', 'user@example.com'); }); - it(`should add SMTP provider password`, () => { cy.visit(smtpPath); cy.get('[data-e2e="add-smtp-password-button"]').click(); @@ -60,5 +58,56 @@ describe('instance notifications', () => { cy.get('h4').contains('Twilio'); cy.get('.state').contains('Inactive'); }); + + it(`should activate SMS provider`, () => { + cy.visit(smsPath); + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Inactive'); + cy.get('[data-e2e="activate-sms-provider-button"]').click(); + cy.shouldConfirmSuccess(); + cy.get('.state').contains('Active'); + }); + + it(`should edit SMS provider`, () => { + cy.visit(smsPath); + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Active'); + cy.get('[data-e2e="new-twilio-button"]').click(); + cy.get('[formcontrolname="sid"]').should('have.value', 'test'); + cy.get('[formcontrolname="senderNumber"]').should('have.value', '2312123132'); + cy.get('[formcontrolname="sid"]').clear().type('test2'); + cy.get('[formcontrolname="senderNumber"]').clear().type('6666666666'); + cy.get('[data-e2e="save-sms-settings-button"]').click(); + cy.shouldConfirmSuccess(); + }); + + it(`should contain edited values`, () => { + cy.visit(smsPath); + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Active'); + cy.get('[data-e2e="new-twilio-button"]').click(); + cy.get('[formcontrolname="sid"]').should('have.value', 'test2'); + cy.get('[formcontrolname="senderNumber"]').should('have.value', '6666666666'); + }); + + it(`should edit SMS provider token`, () => { + cy.visit(smsPath); + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Active'); + cy.get('[data-e2e="new-twilio-button"]').click(); + cy.get('[data-e2e="edit-sms-token-button"]').click(); + cy.get('[data-e2e="notification-setting-password"]').clear().type('newsupertoken'); + cy.get('[data-e2e="save-notification-setting-password-button"]').click(); + cy.shouldConfirmSuccess(); + }); + + it(`should remove SMS provider`, () => { + cy.visit(smsPath); + cy.get('h4').contains('Twilio'); + cy.get('.state').contains('Active'); + cy.get('[data-e2e="remove-sms-provider-button"]').click(); + cy.get('[data-e2e="confirm-dialog-button"]').click(); + cy.shouldConfirmSuccess(); + }); }); }); From 6f82285ad69559422ef6f6ee4ff0ee6c0b45a5cd Mon Sep 17 00:00:00 2001 From: Miguel Cabrerizo <30386061+doncicuto@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:47:44 +0200 Subject: [PATCH 23/48] fix: country flag and phone now in sync (#6727) * fix: country flag and phone now in sync * change default country --------- Co-authored-by: Elio Bischof Co-authored-by: Elio Bischof --- .../user-create/user-create.component.html | 7 +++- .../user-create/user-create.component.ts | 32 ++++++++++++++--- .../auth-user-detail.component.ts | 5 ++- .../edit-dialog/edit-dialog.component.html | 2 +- .../edit-dialog/edit-dialog.component.ts | 35 ++++++++++++++++--- .../phone-detail/phone-detail.component.ts | 2 +- .../user-detail/user-detail.component.ts | 5 ++- console/src/app/utils/formatPhone.ts | 10 +++--- console/src/assets/i18n/bg.json | 4 +-- console/src/assets/i18n/de.json | 4 +-- console/src/assets/i18n/en.json | 4 +-- console/src/assets/i18n/es.json | 4 +-- console/src/assets/i18n/fr.json | 4 +-- console/src/assets/i18n/it.json | 4 +-- console/src/assets/i18n/ja.json | 4 +-- console/src/assets/i18n/mk.json | 4 +-- console/src/assets/i18n/pl.json | 4 +-- console/src/assets/i18n/pt.json | 4 +-- console/src/assets/i18n/zh.json | 4 +-- 19 files changed, 102 insertions(+), 40 deletions(-) diff --git a/console/src/app/pages/users/user-create/user-create.component.html b/console/src/app/pages/users/user-create/user-create.component.html index e74020ea9e..0e357677d5 100644 --- a/console/src/app/pages/users/user-create/user-create.component.html +++ b/console/src/app/pages/users/user-create/user-create.component.html @@ -103,7 +103,12 @@
{{ 'USER.PROFILE.COUNTRY' | translate }} - + diff --git a/console/src/app/pages/users/user-create/user-create.component.ts b/console/src/app/pages/users/user-create/user-create.component.ts index e23e51dcc7..9d43c07a73 100644 --- a/console/src/app/pages/users/user-create/user-create.component.ts +++ b/console/src/app/pages/users/user-create/user-create.component.ts @@ -2,7 +2,7 @@ import { Location } from '@angular/common'; import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; import { Router } from '@angular/router'; -import { Subject } from 'rxjs'; +import { Subject, debounceTime } from 'rxjs'; import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb'; import { Domain } from 'src/app/proto/generated/zitadel/org_pb'; import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; @@ -35,7 +35,11 @@ export class UserCreateComponent implements OnInit, OnDestroy { public user: AddHumanUserRequest.AsObject = new AddHumanUserRequest().toObject(); public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED]; public languages: string[] = supportedLanguages; - public selected: CountryPhoneCode | undefined; + public selected: CountryPhoneCode | undefined = { + countryCallingCode: '1', + countryCode: 'US', + countryName: 'United States of America', + }; public countryPhoneCodes: CountryPhoneCode[] = []; public userForm!: UntypedFormGroup; public pwdForm!: UntypedFormGroup; @@ -145,6 +149,14 @@ export class UserCreateComponent implements OnInit, OnDestroy { }); } }); + + this.phone?.valueChanges.pipe(debounceTime(200)).subscribe((value: string) => { + const phoneNumber = formatPhone(value); + if (phoneNumber) { + this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); + this.phone?.setValue(phoneNumber.phone); + } + }); } public createUser(): void { @@ -175,8 +187,10 @@ export class UserCreateComponent implements OnInit, OnDestroy { if (this.phone && this.phone.value) { // Try to parse number and format it according to country const phoneNumber = formatPhone(this.phone.value); - this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); - humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(phoneNumber.phone)); + if (phoneNumber) { + this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); + humanReq.setPhone(new AddHumanUserRequest.Phone().setPhone(phoneNumber.phone)); + } } this.mgmtService @@ -258,4 +272,14 @@ export class UserCreateComponent implements OnInit, OnDestroy { return; } } + + public compareCountries(i1: CountryPhoneCode, i2: CountryPhoneCode) { + return ( + i1 && + i2 && + i1.countryCallingCode === i2.countryCallingCode && + i1.countryCode == i2.countryCode && + i1.countryName == i2.countryName + ); + } } diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts index fa50b3b419..42731ec643 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts @@ -303,7 +303,10 @@ export class AuthUserDetailComponent implements OnDestroy { public savePhone(phone: string): void { if (this.user?.human) { // Format phone before save (add +) - phone = formatPhone(phone).phone; + const formattedPhone = formatPhone(phone); + if (formattedPhone) { + phone = formattedPhone.phone; + } this.userService .setMyPhone(phone) diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.html index 2aa944959a..924adb58d5 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.html +++ b/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.html @@ -7,7 +7,7 @@
{{ 'USER.PROFILE.COUNTRY' | translate }} - + diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts index 994cdce4dc..54df57e17c 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/edit-dialog/edit-dialog.component.ts @@ -4,6 +4,7 @@ import { MatLegacyDialogRef as MatDialogRef, MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, } from '@angular/material/legacy-dialog'; +import { debounceTime } from 'rxjs'; import { requiredValidator } from 'src/app/modules/form-field/validators/validators'; import { CountryCallingCodesService, CountryPhoneCode } from 'src/app/services/country-calling-codes.service'; import { formatPhone } from 'src/app/utils/formatPhone'; @@ -22,10 +23,14 @@ export class EditDialogComponent implements OnInit { public controlKey = 'editingField'; public isPhone: boolean = false; public isVerified: boolean = false; - public phoneCountry: string = 'CH'; + public phoneCountry: string = 'US'; public dialogForm!: UntypedFormGroup; public EditDialogType: any = EditDialogType; - public selected: CountryPhoneCode | undefined; + public selected: CountryPhoneCode | undefined = { + countryCallingCode: '1', + countryCode: 'US', + countryName: 'United States of America', + }; public countryPhoneCodes: CountryPhoneCode[] = []; constructor( public dialogRef: MatDialogRef, @@ -38,6 +43,16 @@ export class EditDialogComponent implements OnInit { this.dialogForm = new FormGroup({ [this.controlKey]: new UntypedFormControl(data.value, data.validator || requiredValidator), }); + + if (this.isPhone) { + this.ctrl?.valueChanges.pipe(debounceTime(200)).subscribe((value: string) => { + const phoneNumber = formatPhone(value); + if (phoneNumber) { + this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); + this.ctrl?.setValue(phoneNumber.phone); + } + }); + } } public setCountryCallingCode(): void { @@ -52,8 +67,10 @@ export class EditDialogComponent implements OnInit { // Get country phone codes and set selected flag to guessed country or default country this.countryPhoneCodes = this.countryCallingCodesService.getCountryCallingCodes(); const phoneNumber = formatPhone(this.dialogForm.controls[this.controlKey]?.value); - this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); - this.dialogForm.controls[this.controlKey].setValue(phoneNumber.phone); + if (phoneNumber) { + this.selected = this.countryPhoneCodes.find((code) => code.countryCode === phoneNumber.country); + this.dialogForm.controls[this.controlKey].setValue(phoneNumber.phone); + } } } @@ -68,4 +85,14 @@ export class EditDialogComponent implements OnInit { public get ctrl() { return this.dialogForm.get(this.controlKey); } + + public compareCountries(i1: CountryPhoneCode, i2: CountryPhoneCode) { + return ( + i1 && + i2 && + i1.countryCallingCode === i2.countryCallingCode && + i1.countryCode == i2.countryCode && + i1.countryName == i2.countryName + ); + } } diff --git a/console/src/app/pages/users/user-detail/phone-detail/phone-detail.component.ts b/console/src/app/pages/users/user-detail/phone-detail/phone-detail.component.ts index c69980b0e2..9674bab166 100644 --- a/console/src/app/pages/users/user-detail/phone-detail/phone-detail.component.ts +++ b/console/src/app/pages/users/user-detail/phone-detail/phone-detail.component.ts @@ -13,7 +13,7 @@ export class PhoneDetailComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes['phone'].currentValue) { const phoneNumber = formatPhone(changes['phone'].currentValue); - if (this.phone !== phoneNumber.phone) { + if (phoneNumber && this.phone !== phoneNumber.phone) { this.phone = phoneNumber.phone; this.country = phoneNumber.country; } diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index 191115e59f..d197fd7dee 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -365,7 +365,10 @@ export class UserDetailComponent implements OnInit { public savePhone(phone: string): void { if (this.user.id && phone) { // Format phone before save (add +) - phone = formatPhone(phone).phone; + const formattedPhone = formatPhone(phone); + if (formattedPhone) { + phone = formattedPhone.phone; + } this.mgmtUserService .updateHumanPhone(this.user.id, phone) diff --git a/console/src/app/utils/formatPhone.ts b/console/src/app/utils/formatPhone.ts index 2d25be23eb..d5a246667c 100644 --- a/console/src/app/utils/formatPhone.ts +++ b/console/src/app/utils/formatPhone.ts @@ -1,7 +1,7 @@ import { CountryCode, parsePhoneNumber } from 'libphonenumber-js'; -export function formatPhone(phone: string): { phone: string; country: CountryCode } { - const defaultCountry = 'CH'; +export function formatPhone(phone: string): { phone: string; country: CountryCode } | null { + const defaultCountry = 'US'; if (phone) { try { @@ -10,10 +10,10 @@ export function formatPhone(phone: string): { phone: string; country: CountryCod if (phoneNumber) { return { phone: phoneNumber.formatInternational(), country }; } - } catch (error) { - console.error(error); + } catch (e) { + return null; } } - return { phone, country: defaultCountry }; + return null; } diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index e9907811e0..1e42930967 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -267,7 +267,7 @@ "SYMBOLERROR": "Трябва да включва символ или препинателен знак.", "NUMBERERROR": "Трябва да включва цифра.", "PWNOTEQUAL": "Предоставените пароли не съвпадат.", - "PHONE": "Телефонният номер трябва да започва с 00 или ." + "PHONE": "Телефонният номер трябва да започва с +." }, "USER": { "SETTINGS": { @@ -479,7 +479,7 @@ "TITLE": "Профил", "EMAIL": "Електронна поща", "PHONE": "Телефонен номер", - "PHONE_HINT": "Използвайте 00 или символа, последван от кода на държавата, на която се обаждате, или изберете държавата от падащото меню и накрая въведете телефонния номер", + "PHONE_HINT": "Използвайте символа +, последван от кода на държавата, на която се обаждате, или изберете държавата от падащото меню и накрая въведете телефонния номер", "USERNAME": "Потребителско име", "CHANGEUSERNAME": "променям", "CHANGEUSERNAME_TITLE": "Промяна на потребителското име", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index fc3c9633a8..61b0fb152c 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -273,7 +273,7 @@ "SYMBOLERROR": "Muss ein Symbol/Satzzeichen beinhalten.", "NUMBERERROR": "Muss eine Ziffer beinhalten.", "PWNOTEQUAL": "Die Passwörter stimmen nicht überein.", - "PHONE": "Die Telefonnummer muss mit 00 oder + starten." + "PHONE": "Die Telefonnummer muss mit + starten." }, "USER": { "SETTINGS": { @@ -485,7 +485,7 @@ "TITLE": "Profil", "EMAIL": "E-Mail", "PHONE": "Telefonnummer", - "PHONE_HINT": "Verwenden Sie 00 oder das Symbol + gefolgt von der Landesvorwahl des Anrufers oder wählen Sie das Land aus der Dropdown-Liste aus und geben anschließend die Telefonnummer ein", + "PHONE_HINT": "Verwenden das Symbol + gefolgt von der Landesvorwahl des Anrufers oder wählen Sie das Land aus der Dropdown-Liste aus und geben anschließend die Telefonnummer ein", "USERNAME": "Benutzername", "CHANGEUSERNAME": "bearbeiten", "CHANGEUSERNAME_TITLE": "Benutzername ändern", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 5166c33f5e..17cc6cb90a 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -274,7 +274,7 @@ "SYMBOLERROR": "Must include a symbol or punctuation mark.", "NUMBERERROR": "Must include a digit.", "PWNOTEQUAL": "The passwords provided do not match.", - "PHONE": "The phone number must start with 00 or +." + "PHONE": "The phone number must start with +." }, "USER": { "SETTINGS": { @@ -486,7 +486,7 @@ "TITLE": "Profile", "EMAIL": "E-mail", "PHONE": "Phone number", - "PHONE_HINT": "Use 00 or the + symbol followed by the calling country code, or select the country from the dropdown and finally enter the phone number", + "PHONE_HINT": "Use the + symbol followed by the calling country code, or select the country from the dropdown and finally enter the phone number", "USERNAME": "User Name", "CHANGEUSERNAME": "modify", "CHANGEUSERNAME_TITLE": "Change username", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index d4faf3abf4..00add3480a 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -274,7 +274,7 @@ "SYMBOLERROR": "Debe incluir un símbolo o un signo de puntuación.", "NUMBERERROR": "Debe incluir un dígito.", "PWNOTEQUAL": "Las contraseñas proporcionadas no coinciden.", - "PHONE": "El número de teléfono debe comenzar con 00 o +." + "PHONE": "El número de teléfono debe comenzar con +." }, "USER": { "SETTINGS": { @@ -486,7 +486,7 @@ "TITLE": "Perfil", "EMAIL": "Email", "PHONE": "Número de teléfono", - "PHONE_HINT": "Usa 00 o el símbolo + seguido del prefijo del país o selecciona el país del menú desplegable y finalmente introduce el número de teléfono", + "PHONE_HINT": "Usa el símbolo + seguido del prefijo del país o selecciona el país del menú desplegable y finalmente introduce el número de teléfono", "USERNAME": "Nombre de usuario", "CHANGEUSERNAME": "modificar", "CHANGEUSERNAME_TITLE": "Cambiar nombre de usuario", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index bc52d02dd7..3c46abcbed 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -273,7 +273,7 @@ "SYMBOLERROR": "Doit inclure un symbole ou un signe de ponctuation.", "NUMBERERROR": "Doit inclure un chiffre.", "PWNOTEQUAL": "Les mots de passe fournis ne correspondent pas.", - "PHONE": "Le numéro de téléphone doit commencer par 00 ou +." + "PHONE": "Le numéro de téléphone doit commencer par +." }, "USER": { "SETTINGS": { @@ -485,7 +485,7 @@ "TITLE": "Profil", "EMAIL": "Courriel", "PHONE": "Numéro de téléphone", - "PHONE_HINT": "Utilisez 00 ou le symbole + suivi de l'indicatif du pays de l'appelant, ou sélectionnez le pays dans la liste déroulante et saisissez enfin le numéro de téléphone", + "PHONE_HINT": "Utilisez le symbole + suivi de l'indicatif du pays de l'appelant, ou sélectionnez le pays dans la liste déroulante et saisissez enfin le numéro de téléphone", "USERNAME": "Nom de l'utilisateur", "CHANGEUSERNAME": "modifier", "CHANGEUSERNAME_TITLE": "Modifier le nom d'utilisateur", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 635d42c301..cf618d49f6 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -272,7 +272,7 @@ "SYMBOLERROR": "Deve includere un simbolo o un segno di punteggiatura.", "NUMBERERROR": "Deve includere una cifra.", "PWNOTEQUAL": "Le password fornite non corrispondono.", - "PHONE": "Il numero di telefono deve iniziare con 00 o +." + "PHONE": "Il numero di telefono deve iniziare con +." }, "USER": { "SETTINGS": { @@ -484,7 +484,7 @@ "TITLE": "Profilo", "EMAIL": "E-mail", "PHONE": "Numero di telefono", - "PHONE_HINT": "Utilizza 00 o il simbolo + seguito dal prefisso del paese, o seleziona il paese ed inserisci il numero di telefono", + "PHONE_HINT": "Utilizza il simbolo + seguito dal prefisso del paese, o seleziona il paese ed inserisci il numero di telefono", "USERNAME": "Nome utente", "CHANGEUSERNAME": "cambia", "CHANGEUSERNAME_TITLE": "Cambia nome utente", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 3f6bd719a9..9e0ab6d63f 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -274,7 +274,7 @@ "SYMBOLERROR": "シンボルや句読点を含める必要があります。", "NUMBERERROR": "小数点を含める必要があります。", "PWNOTEQUAL": "パスワードが一致しません。", - "PHONE": "電話番号は00か+で始まる必要があります。" + "PHONE": "電話番号は + で始まる必要があります。" }, "USER": { "SETTINGS": { @@ -486,7 +486,7 @@ "TITLE": "プロフィール", "EMAIL": "Eメール", "PHONE": "電話番号", - "PHONE_HINT": "00または+マークの後に通話先の国番号を入力するか、ドロップダウンから国を選択し、最後に電話番号を入力します。", + "PHONE_HINT": "+ マークに続いて電話をかけたい国コードを入力するか、ドロップダウンから国を選択して電話番号を入力します。", "USERNAME": "ユーザー名", "CHANGEUSERNAME": "変更", "CHANGEUSERNAME_TITLE": "ユーザー名の変更", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 40360136b2..4c5f05fed0 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -274,7 +274,7 @@ "SYMBOLERROR": "Мора да содржи симбол или знак за интерпункција.", "NUMBERERROR": "Мора да содржи цифра.", "PWNOTEQUAL": "Внесените лозинки не се совпаѓаат.", - "PHONE": "Телефонскиот број мора да почне со 00 или +." + "PHONE": "Телефонскиот број мора да започнува со +." }, "USER": { "SETTINGS": { @@ -486,7 +486,7 @@ "TITLE": "Профил", "EMAIL": "Е-пошта", "PHONE": "Телефонски број", - "PHONE_HINT": "Користете 00 или + и потоа дополнителниот број на земјата, или изберете ја земјата од листата и на крај внесете го телефонскиот број", + "PHONE_HINT": "Користете + и потоа дополнителниот број на земјата, или изберете ја земјата од листата и на крај внесете го телефонскиот број", "USERNAME": "Корисничко име", "CHANGEUSERNAME": "промени", "CHANGEUSERNAME_TITLE": "Промени корисничко име", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index c78c6eef10..43a0fa385a 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -273,7 +273,7 @@ "SYMBOLERROR": "Musi zawierać symbol lub znak interpunkcyjny.", "NUMBERERROR": "Musi zawierać cyfrę.", "PWNOTEQUAL": "Podane hasła nie są identyczne.", - "PHONE": "Numer telefonu musi zaczynać się od 00 lub +." + "PHONE": "Numer telefonu musi zaczynać się od +." }, "USER": { "SETTINGS": { @@ -485,7 +485,7 @@ "TITLE": "Profil", "EMAIL": "E-mail", "PHONE": "Numer telefonu", - "PHONE_HINT": "Użyj 00 lub symbolu +, a następnie kodu kraju, z którego dzwonisz, lub wybierz kraj z listy rozwijanej i wprowadź numer telefonu.", + "PHONE_HINT": "Użyj symbolu +, a następnie kodu kraju, z którego dzwonisz, lub wybierz kraj z listy rozwijanej i wprowadź numer telefonu.", "USERNAME": "Nazwa użytkownika", "CHANGEUSERNAME": "modyfikuj", "CHANGEUSERNAME_TITLE": "Zmień nazwę użytkownika", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index bd2a648a0a..c79da71aca 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -274,7 +274,7 @@ "SYMBOLERROR": "Deve incluir um símbolo ou caractere de pontuação.", "NUMBERERROR": "Deve incluir um dígito.", "PWNOTEQUAL": "As senhas fornecidas não correspondem.", - "PHONE": "O número de telefone deve começar com 00 ou +." + "PHONE": "O número de telefone deve começar com +." }, "USER": { "SETTINGS": { @@ -486,7 +486,7 @@ "TITLE": "Perfil", "EMAIL": "E-mail", "PHONE": "Número de Telefone", - "PHONE_HINT": "Use 00 ou o símbolo + seguido do código de chamada do país, ou selecione o país na lista suspensa e, em seguida, insira o número de telefone", + "PHONE_HINT": "Use o símbolo + seguido do código de chamada do país, ou selecione o país na lista suspensa e, em seguida, insira o número de telefone", "USERNAME": "Nome de Usuário", "CHANGEUSERNAME": "modificar", "CHANGEUSERNAME_TITLE": "Alterar nome de usuário", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 7d34e7a47f..6b14890087 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -273,7 +273,7 @@ "SYMBOLERROR": "密码必须包含符号或标点符号。", "NUMBERERROR": "密码必须包含数字。", "PWNOTEQUAL": "提供的密码不匹配。", - "PHONE": "电话号码必须以00或+开头。" + "PHONE": "电话号码必须以 + 开头。" }, "USER": { "SETTINGS": { @@ -485,7 +485,7 @@ "TITLE": "信息", "EMAIL": "电子邮件", "PHONE": "手机号码", - "PHONE_HINT": "使用 00 或 + 符号后跟呼叫者的国家代码,或从下拉列表中选择国家,最后输入电话号码", + "PHONE_HINT": "使用+号,后跟呼叫者的国家/地区代码,或从下拉列表中选择国家/地区,最后输入电话号码", "USERNAME": "用户名", "CHANGEUSERNAME": "修改", "CHANGEUSERNAME_TITLE": "修改用户名称", From 93122efe9f7a9b4874a135881fa3cae67fe46838 Mon Sep 17 00:00:00 2001 From: Fabi Date: Tue, 24 Oct 2023 23:19:12 +0200 Subject: [PATCH 24/48] fix: cryptic error message for user not found (#6787) * fix: cryptic error message for user not found * fix: cryptic error message for user not found, fix test --- internal/command/session.go | 2 +- internal/command/session_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/command/session.go b/internal/command/session.go index caf3056f76..045d7e9578 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -262,7 +262,7 @@ func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteMo return nil, err } if humanWriteModel.UserState != domain.UserStateActive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.ie4Ai.NotFound") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.User.NotFound") } return humanWriteModel, nil } diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 7e443a4c53..8892c635b4 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -95,7 +95,7 @@ func TestSessionCommands_getHumanWriteModel(t *testing.T) { }, res: res{ want: nil, - err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.ie4Ai.NotFound"), + err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4b3", "Errors.User.NotFound"), }, }, { From 1fafefc2c1f89b7f1aefee4f5454cf0217218383 Mon Sep 17 00:00:00 2001 From: Justice Chinedu <76141133+Universe-stack@users.noreply.github.com> Date: Wed, 25 Oct 2023 08:23:06 +0100 Subject: [PATCH 25/48] docs: Updated README.md (#6795) Updated README.md I updated the existing Read.me file adding checkmarks for easier reading. I also adjusted some sentences for better grammatical meaning. The overall purpose of these actions is to improve user and customer experience and understanding. Co-authored-by: Fabi --- README.md | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d5a89a5669..91d9ec34e4 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,37 @@

-Do you look for a user management that's quickly set up like Auth0 and open source like Keycloak? + +Are you searching for a user management tool that is quickly set up like Auth0 and open source like Keycloak? Do you have a project that requires multi-tenant user management with self-service for your customers? Look no further — ZITADEL is the identity infrastructure, simplified for you. -We provide you with a wide range of out-of-the-box features to accelerate your project. -Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, LDAP, Passkeys / FIDO2, OTP, U2F, and an unlimited audit trail is there for you, ready to use. +We provide you with a wide range of out-of-the-box features to accelerate your project, including: -With ZITADEL you can rely on a hardened and extensible turnkey solution to solve all of your authentication and authorization needs. +:white_check_mark: Multi-tenancy with branding customization + +:white_check_mark: Secure login + +:white_check_mark: Self-service + +:white_check_mark: OpenID Connect + +:white_check_mark: OAuth2.x + +:white_check_mark: SAML2 + +:white_check_mark: LDAP + +:white_check_mark: Passkeys / FIDO2 + +:white_check_mark: OTP + +:white_check_mark: U2F, and an unlimited audit trail is there for you, ready to use. + + +With ZITADEL, you are assured of a robust and customizable turnkey solution for all your authentication and authorization needs. --- @@ -51,7 +72,7 @@ With ZITADEL you can rely on a hardened and extensible turnkey solution to solve ### Deploy ZITADEL (Self-Hosted) -Deploying ZITADEL locally takes less than 3 minutes. So go ahead and give it a try! +Deploying ZITADEL locally takes less than 3 minutes. Go ahead and give it a try! * [Linux](https://zitadel.com/docs/self-hosting/deploy/linux) * [MacOS](https://zitadel.com/docs/self-hosting/deploy/macos) @@ -67,7 +88,7 @@ See all guides [here](https://zitadel.com/docs/self-hosting/deploy/overview) If you want to experience a hands-free ZITADEL, you should use [ZITADEL Cloud](https://zitadel.cloud). -ZITADEL Cloud comes with a free tier and provides you all the features that you find in the open source version. +ZITADEL Cloud comes with a free tier, providing you with all the same features as the open-source version. Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing). ### Example applications @@ -129,7 +150,7 @@ Track upcoming features on our [roadmap](https://zitadel.com/roadmap). ## How To Contribute -Details about how to contribute you can find in the [Contribution Guide](./CONTRIBUTING.md) +Find details about how you can contribute in our [Contribution Guide](./CONTRIBUTING.md) ## Contributors @@ -161,13 +182,13 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A ## Security -See the policy [here](./SECURITY.md). +You can find our security policy [here](./SECURITY.md). [Technical Advisories](https://zitadel.com/docs/support/technical_advisory) are published regarding major issues with the ZITADEL platform that could potentially impact security or stability in production environments. ## License -See the exact licensing terms [here](./LICENSE) +[here](./LICENSE) are our exact licensing terms. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and limitations under the License. +See our [license](./LICENSE) for detailed information governing permissions and limitations on use. From 73dbf3136862ed97ddda1f531a06389dedd35ed5 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Wed, 25 Oct 2023 12:15:22 +0300 Subject: [PATCH 26/48] Merge pull request from GHSA-954h-jrpm-72pm --- internal/api/assets/asset.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 9830baeb90..9b73a93e7c 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -92,6 +92,8 @@ func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authC verifier.RegisterServer("Assets-API", "assets", AssetsService_AuthMethods) router := mux.NewRouter() + csp := http_mw.SecurityHeaders(&http_mw.DefaultSCP, nil) + router.Use(callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor, csp) router.Use(callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor) RegisterRoutes(router, h) router.PathPrefix("/{owner}").Methods("GET").HandlerFunc(DownloadHandleFunc(h, h.GetFile())) From 1c839e308b086e305a94bd79f3608d10313584aa Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 25 Oct 2023 13:16:34 +0200 Subject: [PATCH 27/48] perf: query projected milestones for onboarding view (#6760) * feat: support list milestones api * show milestones in onboarding view * add authenticated milestone * add icon to login milestone * update main * lint * fix import * fix import * lint * reuse proto milestone type mapping --- cmd/defaults.yaml | 2 + .../onboarding-card.component.html | 9 +- .../onboarding-card.component.ts | 6 +- .../onboarding-card/onboarding-card.module.ts | 4 +- .../onboarding/onboarding.component.html | 13 ++- .../onboarding/onboarding.component.ts | 6 +- .../modules/onboarding/onboarding.module.ts | 4 +- .../milestone-pipe.module.ts} | 8 +- .../milestonePipe.ts} | 16 ++- console/src/app/services/admin.service.ts | 98 +++++++++--------- console/src/app/utils/color.ts | 2 + console/src/app/utils/onboarding.ts | 68 ++++++------- console/src/assets/i18n/bg.json | 17 ++-- console/src/assets/i18n/de.json | 17 ++-- console/src/assets/i18n/en.json | 17 ++-- console/src/assets/i18n/es.json | 17 ++-- console/src/assets/i18n/fr.json | 17 ++-- console/src/assets/i18n/it.json | 17 ++-- console/src/assets/i18n/ja.json | 17 ++-- console/src/assets/i18n/mk.json | 17 ++-- console/src/assets/i18n/pl.json | 17 ++-- console/src/assets/i18n/pt.json | 17 ++-- console/src/assets/i18n/zh.json | 13 ++- internal/api/grpc/admin/milestone.go | 24 +++++ .../api/grpc/admin/milestone_converter.go | 99 +++++++++++++++++++ proto/zitadel/admin.proto | 34 ++++++- proto/zitadel/milestone/v1/milestone.proto | 49 +++++++++ 27 files changed, 445 insertions(+), 180 deletions(-) rename console/src/app/pipes/{event-pipe/event-pipe.module.ts => milestone-pipe/milestone-pipe.module.ts} (72%) rename console/src/app/pipes/{event-pipe/event.pipe.ts => milestone-pipe/milestonePipe.ts} (50%) create mode 100644 internal/api/grpc/admin/milestone.go create mode 100644 internal/api/grpc/admin/milestone_converter.go create mode 100644 proto/zitadel/milestone/v1/milestone.proto diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index a681bedca2..54eaaeeb6a 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -904,6 +904,7 @@ InternalAuthZ: - "project.grant.member.write" - "project.grant.member.delete" - "events.read" + - "milestones.read" - Role: "IAM_OWNER_VIEWER" Permissions: - "iam.read" @@ -929,6 +930,7 @@ InternalAuthZ: - "project.grant.read" - "project.grant.member.read" - "events.read" + - "milestones.read" - Role: "IAM_ORG_MANAGER" Permissions: - "org.read" diff --git a/console/src/app/modules/onboarding-card/onboarding-card.component.html b/console/src/app/modules/onboarding-card/onboarding-card.component.html index 477b27c891..b5bceb9012 100644 --- a/console/src/app/modules/onboarding-card/onboarding-card.component.html +++ b/console/src/app/modules/onboarding-card/onboarding-card.component.html @@ -20,15 +20,18 @@ [routerLink]="action[1].link" [queryParams]="{ id: action[1].fragment }" class="action-element" - [ngClass]="{ done: action[1].event !== undefined }" + [ngClass]="{ done: action[1].reached !== undefined }" >
- check_circle
- {{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }} + {{ 'ONBOARDING.MILESTONES.' + action[0] + '.title' | translate }} keyboard_arrow_right diff --git a/console/src/app/modules/onboarding-card/onboarding-card.component.ts b/console/src/app/modules/onboarding-card/onboarding-card.component.ts index 14b505afb0..8c5fc17de2 100644 --- a/console/src/app/modules/onboarding-card/onboarding-card.component.ts +++ b/console/src/app/modules/onboarding-card/onboarding-card.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { AdminService } from 'src/app/services/admin.service'; -import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding'; +import { ONBOARDING_MILESTONES } from 'src/app/utils/onboarding'; @Component({ selector: 'cnsl-onboarding-card', @@ -11,7 +11,7 @@ import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding'; export class OnboardingCardComponent implements OnInit { public percentageChanged: EventEmitter = new EventEmitter(); public loading$: BehaviorSubject = new BehaviorSubject(false); - public actions = this.adminService.progressEvents; + public actions = this.adminService.progressMilestones; @Output() public dismissedCard: EventEmitter = new EventEmitter(); constructor(public adminService: AdminService) {} @@ -21,6 +21,6 @@ export class OnboardingCardComponent implements OnInit { } ngOnInit() { - this.adminService.loadEvents.next(ONBOARDING_EVENTS); + this.adminService.loadMilestones.next(ONBOARDING_MILESTONES); } } diff --git a/console/src/app/modules/onboarding-card/onboarding-card.module.ts b/console/src/app/modules/onboarding-card/onboarding-card.module.ts index 520c806ae8..58bd99792f 100644 --- a/console/src/app/modules/onboarding-card/onboarding-card.module.ts +++ b/console/src/app/modules/onboarding-card/onboarding-card.module.ts @@ -6,7 +6,7 @@ import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/le import { TranslateModule } from '@ngx-translate/core'; import { RouterModule } from '@angular/router'; -import { EventPipeModule } from 'src/app/pipes/event-pipe/event-pipe.module'; +import { MilestonePipeModule } from 'src/app/pipes/milestone-pipe/milestone-pipe.module'; import { OnboardingCardComponent } from './onboarding-card.component'; @NgModule({ @@ -17,7 +17,7 @@ import { OnboardingCardComponent } from './onboarding-card.component'; TranslateModule, RouterModule, MatProgressSpinnerModule, - EventPipeModule, + MilestonePipeModule, MatTooltipModule, ], exports: [OnboardingCardComponent], diff --git a/console/src/app/modules/onboarding/onboarding.component.html b/console/src/app/modules/onboarding/onboarding.component.html index 8ec6111af8..1fe806f356 100644 --- a/console/src/app/modules/onboarding/onboarding.component.html +++ b/console/src/app/modules/onboarding/onboarding.component.html @@ -27,10 +27,13 @@ [routerLink]="action[1].link" [queryParams]="{ id: action[1].fragment }" class="action-card card" - [ngClass]="{ done: action[1].event !== undefined }" + [ngClass]="{ done: action[1].reached !== undefined }" >
- check_circle
@@ -54,16 +57,16 @@
- {{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }} + {{ 'ONBOARDING.MILESTONES.' + action[0] + '.title' | translate }} {{ - 'ONBOARDING.EVENTS.' + action[0] + '.description' | translate + 'ONBOARDING.MILESTONES.' + action[0] + '.description' | translate }}
- {{ 'ONBOARDING.EVENTS.' + action[0] + '.action' | translate }} + {{ 'ONBOARDING.MILESTONES.' + action[0] + '.action' | translate }} keyboard_arrow_right
diff --git a/console/src/app/modules/onboarding/onboarding.component.ts b/console/src/app/modules/onboarding/onboarding.component.ts index d72f129d89..e0938f59d5 100644 --- a/console/src/app/modules/onboarding/onboarding.component.ts +++ b/console/src/app/modules/onboarding/onboarding.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { AdminService } from 'src/app/services/admin.service'; import { ThemeService } from 'src/app/services/theme.service'; -import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding'; +import { ONBOARDING_MILESTONES } from 'src/app/utils/onboarding'; @Component({ selector: 'cnsl-onboarding', @@ -9,12 +9,12 @@ import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding'; styleUrls: ['./onboarding.component.scss'], }) export class OnboardingComponent { - public actions = this.adminService.progressEvents; + public actions = this.adminService.progressMilestones; constructor( public adminService: AdminService, public themeService: ThemeService, ) { - this.adminService.loadEvents.next(ONBOARDING_EVENTS); + this.adminService.loadMilestones.next(ONBOARDING_MILESTONES); } } diff --git a/console/src/app/modules/onboarding/onboarding.module.ts b/console/src/app/modules/onboarding/onboarding.module.ts index 598d525bd9..7363761b4f 100644 --- a/console/src/app/modules/onboarding/onboarding.module.ts +++ b/console/src/app/modules/onboarding/onboarding.module.ts @@ -9,7 +9,7 @@ import { ShortcutsModule } from 'src/app/modules/shortcuts/shortcuts.module'; import { MatLegacyProgressBarModule } from '@angular/material/legacy-progress-bar'; import { RouterModule } from '@angular/router'; -import { EventPipeModule } from 'src/app/pipes/event-pipe/event-pipe.module'; +import { MilestonePipeModule } from 'src/app/pipes/milestone-pipe/milestone-pipe.module'; import { OnboardingComponent } from './onboarding.component'; @NgModule({ @@ -24,7 +24,7 @@ import { OnboardingComponent } from './onboarding.component'; RouterModule, MatProgressSpinnerModule, MatLegacyProgressBarModule, - EventPipeModule, + MilestonePipeModule, ], exports: [OnboardingComponent], }) diff --git a/console/src/app/pipes/event-pipe/event-pipe.module.ts b/console/src/app/pipes/milestone-pipe/milestone-pipe.module.ts similarity index 72% rename from console/src/app/pipes/event-pipe/event-pipe.module.ts rename to console/src/app/pipes/milestone-pipe/milestone-pipe.module.ts index 16d81152dc..6d887910ce 100644 --- a/console/src/app/pipes/event-pipe/event-pipe.module.ts +++ b/console/src/app/pipes/milestone-pipe/milestone-pipe.module.ts @@ -2,11 +2,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { LocalizedDatePipeModule } from '../localized-date-pipe/localized-date-pipe.module'; import { TimestampToDatePipeModule } from '../timestamp-to-date-pipe/timestamp-to-date-pipe.module'; -import { EventPipe } from './event.pipe'; +import { MilestonePipe } from './milestonePipe'; @NgModule({ - declarations: [EventPipe], + declarations: [MilestonePipe], imports: [CommonModule, TimestampToDatePipeModule, LocalizedDatePipeModule], - exports: [EventPipe], + exports: [MilestonePipe], }) -export class EventPipeModule {} +export class MilestonePipeModule {} diff --git a/console/src/app/pipes/event-pipe/event.pipe.ts b/console/src/app/pipes/milestone-pipe/milestonePipe.ts similarity index 50% rename from console/src/app/pipes/event-pipe/event.pipe.ts rename to console/src/app/pipes/milestone-pipe/milestonePipe.ts index 932a1c470b..de556bffa1 100644 --- a/console/src/app/pipes/event-pipe/event.pipe.ts +++ b/console/src/app/pipes/milestone-pipe/milestonePipe.ts @@ -1,22 +1,18 @@ import { Pipe, PipeTransform } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { Event } from 'src/app/proto/generated/zitadel/event_pb'; import { LocalizedDatePipe } from '../localized-date-pipe/localized-date.pipe'; import { TimestampToDatePipe } from '../timestamp-to-date-pipe/timestamp-to-date.pipe'; +import { Milestone } from '../../proto/generated/zitadel/milestone/v1/milestone_pb'; @Pipe({ - name: 'event', + name: 'milestone', }) -export class EventPipe implements PipeTransform { +export class MilestonePipe implements PipeTransform { constructor(private translateService: TranslateService) {} - public transform(event?: Event.AsObject): any { - if (event && event.editor?.displayName && event.creationDate) { - const timestampToDate = new TimestampToDatePipe().transform(event.creationDate); - const datePipeOutput = new LocalizedDatePipe(this.translateService).transform(timestampToDate); - return `${event.editor?.displayName} last changed it on ${datePipeOutput}`; - } else if (event && event.creationDate) { - const timestampToDate = new TimestampToDatePipe().transform(event.creationDate); + public transform(milestone?: Milestone.AsObject): any { + if (milestone && milestone.reachedDate) { + const timestampToDate = new TimestampToDatePipe().transform(milestone.reachedDate); const datePipeOutput = new LocalizedDatePipe(this.translateService).transform(timestampToDate); return `done on ${datePipeOutput}`; } else { diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index 0b173bdaa8..e2ef755527 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -152,6 +152,8 @@ import { ListLoginPolicyMultiFactorsResponse, ListLoginPolicySecondFactorsRequest, ListLoginPolicySecondFactorsResponse, + ListMilestonesRequest, + ListMilestonesResponse, ListProvidersRequest, ListProvidersResponse, ListSecretGeneratorsRequest, @@ -296,85 +298,77 @@ import { SearchQuery } from '../proto/generated/zitadel/member_pb'; import { ListQuery } from '../proto/generated/zitadel/object_pb'; import { GrpcService } from './grpc.service'; import { StorageLocation, StorageService } from './storage.service'; +import { + IsReachedQuery, + Milestone, + MilestoneQuery, + MilestoneType, +} from '../proto/generated/zitadel/milestone/v1/milestone_pb'; export interface OnboardingActions { order: number; - eventType: string; - oneof: string[]; - link: string | string[]; + milestoneType: MilestoneType; + link: string; fragment?: string | undefined; iconClasses?: string; darkcolor: string; lightcolor: string; - aggregateType: string; } -type OnboardingEvent = { +type OnboardingMilestone = { order: number; link: string; fragment: string | undefined; - event: Event.AsObject | undefined; + reached: Milestone.AsObject | undefined; iconClasses?: string; darkcolor: string; lightcolor: string; }; -type OnboardingEventEntries = Array<[string, OnboardingEvent]> | []; +type OnboardingMilestoneEntries = Array<[string, OnboardingMilestone]> | []; @Injectable({ providedIn: 'root', }) export class AdminService { + private readonly milestoneTypePrefixLength = 'MILESTONE_TYPE_'.length; public hideOnboarding: boolean = false; - public loadEvents: Subject = new Subject(); + public loadMilestones: Subject = new Subject(); public onboardingLoading: BehaviorSubject = new BehaviorSubject(false); - public progressEvents$: Observable = this.loadEvents.pipe( + public progressMilestones$: Observable = this.loadMilestones.pipe( tap(() => this.onboardingLoading.next(true)), switchMap((actions) => { - const searchForTypes = actions.map((oe) => oe.oneof).flat(); - const aggregateTypes = actions.map((oe) => oe.aggregateType); - const eventsReq = new ListEventsRequest() - .setAsc(true) - .setEventTypesList(searchForTypes) - .setAggregateTypesList(aggregateTypes) - .setAsc(false); - return from(this.listEvents(eventsReq)).pipe( - map((events) => { - const el = events.toObject().eventsList.filter((e) => e.editor?.service !== 'System-API' && e.editor?.userId); - - let obj: { [type: string]: OnboardingEvent } = {}; + const milestonesListQuery = new ListQuery(); + milestonesListQuery.setAsc(true); + milestonesListQuery.setLimit(20); + const milestoneIsReachedQuery = new IsReachedQuery().setReached(true); + const milestonesQuery = new MilestoneQuery().setIsReachedQuery(milestoneIsReachedQuery); + const milestonesReq = new ListMilestonesRequest().setQuery(milestonesListQuery).setQueriesList([milestonesQuery]); + return from(this.listMilestones(milestonesReq)).pipe( + map((reachedMilestones) => { + let obj: { [type: string]: OnboardingMilestone } = {}; actions.map((action) => { - const filtered = el.filter((event) => event.type?.type && action.oneof.includes(event.type.type)); - (obj as any)[action.eventType] = filtered.length - ? { - order: action.order, - link: action.link, - fragment: action.fragment, - event: filtered[0], - iconClasses: action.iconClasses, - darkcolor: action.darkcolor, - lightcolor: action.lightcolor, - } - : { - order: action.order, - link: action.link, - fragment: action.fragment, - event: undefined, - iconClasses: action.iconClasses, - darkcolor: action.darkcolor, - lightcolor: action.lightcolor, - }; + obj[Object.keys(MilestoneType)[action.milestoneType].substring(this.milestoneTypePrefixLength)] = { + order: action.order, + link: action.link, + fragment: action.fragment, + iconClasses: action.iconClasses, + darkcolor: action.darkcolor, + lightcolor: action.lightcolor, + reached: reachedMilestones.resultList.find((reached) => { + return reached.type.valueOf() == action.milestoneType; + }), + }; }); - const toArray = Object.entries(obj).sort(([key0, a], [key1, b]) => a.order - b.order); - const toDo = toArray.filter(([key, value]) => value.event === undefined); - const done = toArray.filter(([key, value]) => !!value.event); + const toDo = toArray.filter(([key, value]) => value.reached === undefined); + const done = toArray.filter(([key, value]) => !!value.reached); return [...toDo, ...done]; }), - tap((events) => { - const total = events.length; - const done = events.map(([type, value]) => value.event !== undefined).filter((res) => !!res).length; + tap((milestones) => { + const total = milestones.length; + const done = milestones.map(([type, value]) => value.reached !== undefined).filter((res) => !!res).length; const percentage = Math.round((done / total) * 100); this.progressDone.next(done); this.progressTotal.next(total); @@ -390,7 +384,9 @@ export class AdminService { }), ); - public progressEvents: BehaviorSubject = new BehaviorSubject([]); + public progressMilestones: BehaviorSubject = new BehaviorSubject( + [], + ); public progressPercentage: BehaviorSubject = new BehaviorSubject(0); public progressDone: BehaviorSubject = new BehaviorSubject(0); public progressTotal: BehaviorSubject = new BehaviorSubject(0); @@ -400,7 +396,7 @@ export class AdminService { private readonly grpcService: GrpcService, private storageService: StorageService, ) { - this.progressEvents$.subscribe(this.progressEvents); + this.progressMilestones$.subscribe(this.progressMilestones); this.hideOnboarding = this.storageService.getItem('onboarding-dismissed', StorageLocation.local) === 'true' ? true : false; @@ -1254,4 +1250,8 @@ export class AdminService { return this.grpcService.admin.updateIAMMember(req, null).then((resp) => resp.toObject()); } + + public listMilestones(req: ListMilestonesRequest): Promise { + return this.grpcService.admin.listMilestones(req, null).then((resp) => resp.toObject()); + } } diff --git a/console/src/app/utils/color.ts b/console/src/app/utils/color.ts index 5e807adac4..b76982d4fd 100644 --- a/console/src/app/utils/color.ts +++ b/console/src/app/utils/color.ts @@ -25,6 +25,8 @@ export const COLORS = [ { 500: '#d946ef', 200: '#f5d0fe', 300: '#f0abfc', 600: '#c026d3', 700: '#a21caf', 900: '#701a75' }, { 500: '#ec4899', 200: '#fbcfe8', 300: '#f9a8d4', 600: '#db2777', 700: '#be185d', 900: '#831843' }, { 500: '#f43f5e', 200: '#fecdd3', 300: '#fda4af', 600: '#e11d48', 700: '#be123c', 900: '#881337' }, + { 500: '#A89F91', 200: '#D4CDC6', 300: '#BFB6AC', 600: '#8F8378', 700: '#736A60', 900: '#4F4A40' }, + { 500: '#BA9F88', 200: '#E8D3C5', 300: '#D4BAA7', 600: '#9C7A68', 700: '#8A6E5D', 900: '#5F4C42' }, ]; export const WEB_APP_COLOR: Color = COLORS[6]; diff --git a/console/src/app/utils/onboarding.ts b/console/src/app/utils/onboarding.ts index f93c11db1b..70b77f4b66 100644 --- a/console/src/app/utils/onboarding.ts +++ b/console/src/app/utils/onboarding.ts @@ -1,5 +1,6 @@ import { OnboardingActions } from '../services/admin.service'; import { COLORS } from './color'; +import { MilestoneType } from '../proto/generated/zitadel/milestone/v1/milestone_pb'; const reddark: string = COLORS[0][700]; const redlight = COLORS[0][200]; @@ -19,67 +20,66 @@ const purplelight = COLORS[12][200]; const pinkdark: string = COLORS[15][700]; const pinklight = COLORS[15][200]; -export const ONBOARDING_EVENTS: OnboardingActions[] = [ +const sthdark: string = COLORS[18][700]; +const sthlight = COLORS[18][200]; + +export const ONBOARDING_MILESTONES: OnboardingActions[] = [ { order: 0, - eventType: 'project.added', - oneof: ['project.added'], - link: ['/projects/create'], + milestoneType: MilestoneType.MILESTONE_TYPE_PROJECT_CREATED, + link: '/projects/create', iconClasses: 'las la-database', darkcolor: greendark, lightcolor: greenlight, - aggregateType: 'project', }, { order: 1, - eventType: 'project.application.added', - oneof: ['project.application.added'], - link: ['/projects/app-create'], + milestoneType: MilestoneType.MILESTONE_TYPE_APPLICATION_CREATED, + link: '/projects/app-create', iconClasses: 'lab la-openid', darkcolor: purpledark, lightcolor: purplelight, - aggregateType: 'project', - }, - { - order: 2, - eventType: 'user.human.added', - oneof: ['user.human.added'], - link: ['/users/create'], - iconClasses: 'las la-user', - darkcolor: bluedark, - lightcolor: bluelight, - aggregateType: 'user', }, { order: 3, - eventType: 'user.grant.added', - oneof: ['user.grant.added'], - link: ['/grant-create'], + milestoneType: MilestoneType.MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_APPLICATION, + link: 'https://zitadel.com/docs/guides/integrate/login-users', + iconClasses: 'las la-sign-in-alt', + darkcolor: sthdark, + lightcolor: sthlight, + } /* + { + order: 4, + milestoneType: 'user.human.added', + link: '/users/create', + iconClasses: 'las la-user', + darkcolor: bluedark, + lightcolor: bluelight, + }, + { + order: 5, + milestoneType: 'user.grant.added', + link: '/grant-create', iconClasses: 'las la-shield-alt', darkcolor: reddark, lightcolor: redlight, - aggregateType: 'user_grant', }, { - order: 4, - eventType: 'instance.policy.label.added', - oneof: ['instance.policy.label.added', 'instance.policy.label.changed'], - link: ['/settings'], + order: 6, + milestoneType: 'instance.policy.label.added', + link: '/settings', fragment: 'branding', iconClasses: 'las la-swatchbook', darkcolor: pinkdark, lightcolor: pinklight, - aggregateType: 'instance', }, { - order: 5, - eventType: 'instance.smtp.config.added', - oneof: ['instance.smtp.config.added', 'instance.smtp.config.changed'], - link: ['/settings'], + order: 7, + milestoneType: 'instance.smtp.config.added', + link: '/settings', fragment: 'smtpprovider', iconClasses: 'las la-envelope', darkcolor: yellowdark, lightcolor: yellowlight, - aggregateType: 'instance', - }, + },*/, ]; diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 1e42930967..482a479a17 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -51,7 +51,7 @@ "TITLE": "Пуснете своя ZITADEL да работи", "DESCRIPTION": "Този контролен списък помага да настроите вашия екземпляр и ви насочва през най-важните стъпки" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Настройте марката си", "description": "Определете цвета и формата на вашето логин и качете вашето лого и икони.", @@ -62,15 +62,20 @@ "description": "Задайте свои собствени настройки на пощенския сървър.", "action": "Настройка на SMTP" }, - "project.added": { + "PROJECT_CREATED": { "title": "Създайте проект", "description": "Добавете проект и определете неговите роли и пълномощия.", "action": "Създайте проект" }, - "project.application.added": { - "title": "Създайте приложение", - "description": "Създайте уеб, естествено, api или saml приложение и настройте своя поток за удостоверяване.", - "action": "Създаване на приложение" + "APPLICATION_CREATED": { + "title": "Регистрирайте приложението си", + "description": "Регистрирайте вашето уеб, естествено, api или saml приложение и настройте поток за удостоверяване.", + "action": "Регистрирайте приложението" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Влезте в приложението си", + "description": "Интегрирайте приложението си с ZITADEL за удостоверяване и го тествайте, като влезете с администраторския си потребител.", + "action": "Влезте" }, "user.human.added": { "title": "Добавете потребители", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 61b0fb152c..fec4423585 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -51,7 +51,7 @@ "TITLE": "Bringe deine Instanz zum Laufen", "DESCRIPTION": "Diese Checkliste hilft bei der Einrichtung Ihrer Instanz und führt Sie durch die wichtigsten Schritte" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Branding anpassen", "description": "Definiere Farben und Form des Login-UIs und uploade deine Logos und Icons.", @@ -62,15 +62,20 @@ "description": "Konfiguriere deinen Mailserver.", "action": "SMTP einrichten" }, - "project.added": { + "PROJECT_CREATED": { "title": "Erstelle ein Projekt", "description": "Erstelle dein erstes Projekt und definiere Rollen", "action": "Projekt erstellen" }, - "project.application.added": { - "title": "Erstelle eine App", - "description": "Erstelle deine erste Web-, native, API oder SAML-applikation und konfiguriere den Authentification-flow.", - "action": "App erstellen" + "APPLICATION_CREATED": { + "title": "Registriere deine App", + "description": "Registriere deine erste Web-, native, API oder SAML-Applikation und konfiguriere den Authentification-flow.", + "action": "App registrieren" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Logge dich in deine App ein", + "description": "Integriere deine Applikation mit ZITADEL für die Authentifizierung und teste es, indem du dich mit deinem Admin-Benutzer einloggst.", + "action": "Einloggen" }, "user.human.added": { "title": "Erfasse Benutzer", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 17cc6cb90a..dbcff16583 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -51,7 +51,7 @@ "TITLE": "Get your ZITADEL running", "DESCRIPTION": "This checklist helps to setup your instance and guides your through the most essential steps" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Setup your brand", "description": "Define coloring and shape of your login and upload your logo and icons.", @@ -62,15 +62,20 @@ "description": "Set your own mail server settings.", "action": "Setup SMTP" }, - "project.added": { + "PROJECT_CREATED": { "title": "Create a project", "description": "Add a project and define its roles and authorizations.", "action": "Create project" }, - "project.application.added": { - "title": "Create an application", - "description": "Create a web, native, api or saml application and setup your authentication flow.", - "action": "Create app" + "APPLICATION_CREATED": { + "title": "Register your app", + "description": "Register your web, native, api or saml application and setup an authentication flow.", + "action": "Register app" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Log in to your app", + "description": "Integrate your application with ZITADEL for authentication and test it by logging in with your admin user.", + "action": "Log in" }, "user.human.added": { "title": "Add users", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 00add3480a..cae80d082c 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -51,7 +51,7 @@ "TITLE": "Ponte en marcha con ZITADEL", "DESCRIPTION": "Esta lista de tareas te ayuda a configurar tu instancia y te guía por los pasos más esenciales" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Configura tu imagen de marca", "description": "Define el esquema de colores, da forma a tu inicio de sesión y sube tu logo y tus iconos.", @@ -62,15 +62,20 @@ "description": "Introduce la configuración de tu propio servidor de correo.", "action": "Configurar SMTP" }, - "project.added": { + "PROJECT_CREATED": { "title": "Crea tu primer proyecto", "description": "Añade tu primer proyecto y define sus roles y autorizaciones.", "action": "Crear proyecto" }, - "project.application.added": { - "title": "Crea tu primera aplicación", - "description": "Crea una aplicación web, nativa, api o saml y configura tu flujo de autenticación.", - "action": "Crear app" + "APPLICATION_CREATED": { + "title": "Registra tu aplicación", + "description": "Registra tu aplicación web, nativa, api o saml y configura tu flujo de autenticación.", + "action": "Registrar app" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Inicia sesión en tu aplicación", + "description": "Integra tu aplicación con ZITADEL para la autenticación y pruébala iniciando sesión con tu usuario administrador.", + "action": "Iniciar sesión" }, "user.human.added": { "title": "Añade usuarios", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 3c46abcbed..87f94c74c1 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -51,7 +51,7 @@ "TITLE": "Faites fonctionner votre ZITADEL", "DESCRIPTION": "Cette liste de contrôle vous aide à configurer votre instance et vous guide à travers les étapes les plus essentielles." }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Créez votre marque", "description": "Définissez la couleur et la forme de votre connexion et téléchargez votre logo et vos icônes.", @@ -62,15 +62,20 @@ "description": "Définissez paramètres de serveur de messagerie", "action": "Configurez" }, - "project.added": { + "PROJECT_CREATED": { "title": "Créez projet", "description": "Ajoutez projet et définissez ses rôles et autorisations.", "action": "Créez projet" }, - "project.application.added": { - "title": "Créez votre première application", - "description": "Créez une application web, native, api ou saml et configurez votre flux d'authentification.", - "action": "Créez application" + "APPLICATION_CREATED": { + "title": "Enregistrez votre application", + "description": "Enregistrez votre application web, native, api ou saml et configurez un flux d'authentification.", + "action": "Enregistrez l'application" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Connectez-vous à votre application", + "description": "Intégrez votre application avec ZITADEL pour l'authentification et testez-la en vous connectant avec votre utilisateur administrateur.", + "action": "Connexion" }, "user.human.added": { "title": "Ajouter des utilisateurs", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index cf618d49f6..998ea6a662 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -51,7 +51,7 @@ "TITLE": "Fate funzionare il vostro ZITADEL", "DESCRIPTION": "Questa lista di azioni aiuta a configurare la vostra istanza e vi guida attraverso i passaggi più essenziali." }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Imposta il tuo marchio", "description": "Definisci la colorazione e il design del vostro login e caricate il vostro logo e le vostre icone.", @@ -62,15 +62,20 @@ "description": "Imposta il proprio server di posta", "action": "Configura SMTP" }, - "project.added": { + "PROJECT_CREATED": { "title": "Crea il tuo primo progetto", "description": "Aggiungere il primo progetto e definire i ruoli e le autorizzazioni.", "action": "Crea progetto" }, - "project.application.added": { - "title": "Crea la tua prima applicazione", - "description": "Crea un'applicazione web, nativa, api o saml e imposta il flusso di autenticazione.", - "action": "Crea applicazione" + "APPLICATION_CREATED": { + "title": "Registra la tua app", + "description": "Registra la tua applicazione web, nativa, api o saml e configura un flusso di autenticazione.", + "action": "Registra app" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Accedi alla tua app", + "description": "Integra la tua applicazione con ZITADEL per l'autenticazione e testala accedendo con il tuo utente amministratore.", + "action": "Accedi" }, "user.human.added": { "title": "Aggiungi utenti", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 9e0ab6d63f..69f1d9c1fb 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -51,7 +51,7 @@ "TITLE": "ZITADELの起動", "DESCRIPTION": "このチェックリストを使用して、重要な手順を確認しながらインスタンスをセットアップします。" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "ブランドをセットアップする", "description": "ログインの色と形状を定義し、ロゴとアイコンをアップロードします。", @@ -62,15 +62,20 @@ "description": "独自のメールサーバーを設定します。", "action": "SMTP 設定を設定する" }, - "project.added": { + "PROJECT_CREATED": { "title": "最初のプロジェクトを作成する", "description": "最初のプロジェクトを追加し、ロールと認証を定義します。", "action": "プロジェクトを作成" }, - "project.application.added": { - "title": "最初のアプリケーションを作成する", - "description": "Web、ネイティブ、API、またはSAMLアプリケーションを作成し、認証フローをセットアップします。", - "action": "アプリケーションを作成" + "APPLICATION_CREATED": { + "title": "アプリを登録する", + "description": "Web、ネイティブ、API、またはSAMLアプリケーションを登録し、認証フローをセットアップします。", + "action": "アプリを登録する" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "アプリにログインする", + "description": "アプリケーションをZITADELと統合して認証し、管理者ユーザーでログインしてテストします。", + "action": "ログイン" }, "user.human.added": { "title": "ユーザーを追加する", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 4c5f05fed0..c152cb14b5 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -51,7 +51,7 @@ "TITLE": "Почнете со ZITADEL", "DESCRIPTION": "Оваа листа со чекори помага при подесувањето на вашата инстанца и ве води низ најважните чекори" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Подесете го вашиот бренд", "description": "Дефинирајте боја и форма за вашиот процез за најава и прикачете ги вашите лого и икони.", @@ -62,15 +62,20 @@ "description": "Подесете го вашиот сервер за е-пошта.", "action": "Подеси SMTP" }, - "project.added": { + "PROJECT_CREATED": { "title": "Креирајте проект", "description": "Додадете проект и дефинирајте ги неговите улоги и овластувања.", "action": "Креирај проект" }, - "project.application.added": { - "title": "Креирајте апликација", - "description": "Креирајте веб, нативна, API или SAML апликација и подесете го вашите автентикациски правила.", - "action": "Креирај апликација" + "APPLICATION_CREATED": { + "title": "Регистрирајте ја вашата апликација", + "description": "Регистрирајте ја вашата веб, нативна, API или SAML апликација и подесете ја автентикацијата.", + "action": "Регистрирај апликација" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Најавете се во вашата апликација", + "description": "Интегрирајте ја вашата апликација со ZITADEL за автентикација и тестирајте ја со најавување со вашиот администраторски корисник.", + "action": "Најави се" }, "user.human.added": { "title": "Додадете корисници", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 43a0fa385a..02c10021ce 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -51,7 +51,7 @@ "TITLE": "Uruchom swój ZITADEL", "DESCRIPTION": "Ta lista kontrolna pomoże Ci skonfigurować instancję i poprowadzi Cię przez najważniejsze kroki." }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Skonfiguruj swoją markę", "description": "Zdefiniuj kolorystykę i kształt swojego loginu oraz wgraj swoje logo i ikony.", @@ -62,15 +62,20 @@ "description": "Ustawienie własnego serwera pocztowego", "action": "skonfiguruj ustawienia SMTP" }, - "project.added": { + "PROJECT_CREATED": { "title": "Stwórz swój pierwszy projekt", "description": "Dodaj swój pierwszy projekt i określ jego role i uprawnienia.", "action": "Utwórz projekt" }, - "project.application.added": { - "title": "Utwórz swoją pierwszą aplikację", - "description": "Utwórz aplikację internetową, natywną, api lub saml i skonfiguruj swój przepływ uwierzytelniania.", - "action": "Utwórz aplikację" + "APPLICATION_CREATED": { + "title": "Zarejestruj swoją aplikację", + "description": "Zarejestruj swoją aplikację webową, natywną, API lub SAML i skonfiguruj przepływ uwierzytelniania.", + "action": "Zarejestruj aplikację" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Zaloguj się do swojej aplikacji", + "description": "Zintegruj swoją aplikację z ZITADEL w celu uwierzytelniania i przetestuj ją, logując się za pomocą swojego użytkownika administratora.", + "action": "Zaloguj się" }, "user.human.added": { "title": "Dodaj użytkowników", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index c79da71aca..07deb19921 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -51,7 +51,7 @@ "TITLE": "Inicie o ZITADEL", "DESCRIPTION": "Esta lista de verificação ajuda a configurar sua instância e orienta você nas etapas mais essenciais" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "Configure sua marca", "description": "Defina cores e forma para o seu login e faça o upload do seu logotipo e ícones.", @@ -62,15 +62,20 @@ "description": "Configure as configurações do seu próprio servidor de e-mail.", "action": "Configurar SMTP" }, - "project.added": { + "PROJECT_CREATED": { "title": "Crie um projeto", "description": "Adicione um projeto e defina suas funções e autorizações.", "action": "Criar projeto" }, - "project.application.added": { - "title": "Crie um aplicativo", - "description": "Crie um aplicativo da web, nativo, API ou SAML e configure o fluxo de autenticação.", - "action": "Criar aplicativo" + "APPLICATION_CREATED": { + "title": "Registre seu aplicativo", + "description": "Registre seu aplicativo web, nativo, api ou saml e configure um fluxo de autenticação.", + "action": "Registrar aplicativo" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Faça login no seu aplicativo", + "description": "Integre seu aplicativo com o ZITADEL para autenticação e teste-o fazendo login com seu usuário administrador.", + "action": "Faça login" }, "user.human.added": { "title": "Adicione usuários", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 6b14890087..626dae19aa 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -51,7 +51,7 @@ "TITLE": "让你的ZITADEL运转起来", "DESCRIPTION": "这份清单有助于设置你的实例,并指导你完成最重要的步骤" }, - "EVENTS": { + "MILESTONES": { "instance.policy.label.added": { "title": "设置你的品牌", "description": "定义你的登录的颜色和形状,上传你的标志和图标。", @@ -62,16 +62,21 @@ "description": "设置你自己的邮件服务器设置", "action": "设置 SMTP 设置" }, - "project.added": { + "PROJECT_CREATED": { "title": "创建你的第一个项目", "description": "添加你的第一个项目并定义其角色和授权。", "action": "创建项目" }, - "project.application.added": { - "title": "创建你的第一个应用程序", + "APPLICATION_CREATED": { + "title": "注册你的应用程序", "description": "创建一个web、native、api或saml应用程序并设置你的认证流程。", "action": "创建应用程序" }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "登录你的应用程序", + "description": "将你的应用程序与 ZITADEL 集成以进行身份验证,并通过使用管理员用户登录来测试它。", + "action": "登录" + }, "user.human.added": { "title": "添加用户", "description": "添加你的应用程序用户", diff --git a/internal/api/grpc/admin/milestone.go b/internal/api/grpc/admin/milestone.go new file mode 100644 index 0000000000..e06e5c15f0 --- /dev/null +++ b/internal/api/grpc/admin/milestone.go @@ -0,0 +1,24 @@ +package admin + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + object_pb "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/pkg/grpc/admin" +) + +func (s *Server) ListMilestones(ctx context.Context, req *admin.ListMilestonesRequest) (*admin.ListMilestonesResponse, error) { + queries, err := listMilestonesToModel(authz.GetInstance(ctx).InstanceID(), req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchMilestones(ctx, []string{authz.GetInstance(ctx).InstanceID()}, queries) + if err != nil { + return nil, err + } + return &admin.ListMilestonesResponse{ + Result: milestoneViewsToPb(resp.Milestones), + Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.LastRun), + }, nil +} diff --git a/internal/api/grpc/admin/milestone_converter.go b/internal/api/grpc/admin/milestone_converter.go new file mode 100644 index 0000000000..0419cd3fe0 --- /dev/null +++ b/internal/api/grpc/admin/milestone_converter.go @@ -0,0 +1,99 @@ +package admin + +import ( + "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/repository/milestone" + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + milestone_pb "github.com/zitadel/zitadel/pkg/grpc/milestone" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func listMilestonesToModel(instanceID string, req *admin_pb.ListMilestonesRequest) (*query.MilestonesSearchQueries, error) { + offset, limit, asc := object.ListQueryToModel(req.Query) + queries, err := milestoneQueriesToModel(req.GetQueries()) + instanceIDQuery, err := query.NewTextQuery(query.MilestoneInstanceIDColID, instanceID, query.TextEquals) + if err != nil { + return nil, err + } + queries = append(queries, instanceIDQuery) + return &query.MilestonesSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: milestoneFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func milestoneQueriesToModel(queries []*milestone_pb.MilestoneQuery) (q []query.SearchQuery, err error) { + q = make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = milestoneQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func milestoneQueryToModel(milestoneQuery *milestone_pb.MilestoneQuery) (query.SearchQuery, error) { + switch q := milestoneQuery.Query.(type) { + case *milestone_pb.MilestoneQuery_IsReachedQuery: + if q.IsReachedQuery.GetReached() { + return query.NewNotNullQuery(query.MilestoneReachedDateColID) + } + return query.NewIsNullQuery(query.MilestoneReachedDateColID) + default: + return nil, errors.ThrowInvalidArgument(nil, "ADMIN-sE7pc", "List.Query.Invalid") + } +} + +func milestoneFieldNameToSortingColumn(field milestone_pb.MilestoneFieldName) query.Column { + switch field { + case milestone_pb.MilestoneFieldName_MILESTONE_FIELD_NAME_REACHED_DATE: + return query.MilestoneReachedDateColID + default: + return query.MilestoneTypeColID + } +} + +func milestoneViewsToPb(milestones []*query.Milestone) []*milestone_pb.Milestone { + resp := make([]*milestone_pb.Milestone, len(milestones)) + for i, idp := range milestones { + resp[i] = modelMilestoneViewToPb(idp) + } + return resp +} + +func modelMilestoneViewToPb(m *query.Milestone) *milestone_pb.Milestone { + mspb := &milestone_pb.Milestone{ + Type: modelMilestoneTypeToPb(m.Type), + } + if !m.ReachedDate.IsZero() { + mspb.ReachedDate = timestamppb.New(m.ReachedDate) + } + return mspb +} + +func modelMilestoneTypeToPb(t milestone.Type) milestone_pb.MilestoneType { + switch t { + case milestone.InstanceCreated: + return milestone_pb.MilestoneType_MILESTONE_TYPE_INSTANCE_CREATED + case milestone.AuthenticationSucceededOnInstance: + return milestone_pb.MilestoneType_MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_INSTANCE + case milestone.ProjectCreated: + return milestone_pb.MilestoneType_MILESTONE_TYPE_PROJECT_CREATED + case milestone.ApplicationCreated: + return milestone_pb.MilestoneType_MILESTONE_TYPE_APPLICATION_CREATED + case milestone.AuthenticationSucceededOnApplication: + return milestone_pb.MilestoneType_MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_APPLICATION + case milestone.InstanceDeleted: + return milestone_pb.MilestoneType_MILESTONE_TYPE_INSTANCE_DELETED + default: + return milestone_pb.MilestoneType_MILESTONE_TYPE_UNSPECIFIED + } +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index c1f32616b2..c7a2faab6f 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -14,6 +14,7 @@ import "zitadel/event.proto"; import "zitadel/management.proto"; import "zitadel/v1.proto"; import "zitadel/message.proto"; +import "zitadel/milestone/v1/milestone.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -3773,6 +3774,23 @@ service AdminService { permission: "iam.feature.write"; }; } + + rpc ListMilestones(ListMilestonesRequest) returns (ListMilestonesResponse) { + option (google.api.http) = { + post: "/milestones/_search"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "milestones.read"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Milestones"; + summary: "Search Milestones"; + description: "Returns a list of reached instance usage milestones." + }; + } } @@ -7892,4 +7910,18 @@ message ActivateFeatureLoginDefaultOrgRequest {} message ActivateFeatureLoginDefaultOrgResponse { zitadel.v1.ObjectDetails details = 1; -} \ No newline at end of file +} + +message ListMilestonesRequest { + //list limitations and ordering + zitadel.v1.ListQuery query = 1; + // the field the result is sorted + zitadel.milestone.v1.MilestoneFieldName sorting_column = 2; + //criteria the client is looking for + repeated zitadel.milestone.v1.MilestoneQuery queries = 3; +} + +message ListMilestonesResponse { + zitadel.v1.ListDetails details = 1; + repeated zitadel.milestone.v1.Milestone result = 2; +} diff --git a/proto/zitadel/milestone/v1/milestone.proto b/proto/zitadel/milestone/v1/milestone.proto new file mode 100644 index 0000000000..36e19beeb7 --- /dev/null +++ b/proto/zitadel/milestone/v1/milestone.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +import "zitadel/object.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +package zitadel.milestone.v1; + +option go_package ="github.com/zitadel/zitadel/pkg/grpc/milestone"; + +enum MilestoneType { + MILESTONE_TYPE_UNSPECIFIED = 0; + MILESTONE_TYPE_INSTANCE_CREATED = 1; + MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_INSTANCE = 2; + MILESTONE_TYPE_PROJECT_CREATED = 3; + MILESTONE_TYPE_APPLICATION_CREATED = 4; + MILESTONE_TYPE_AUTHENTICATION_SUCCEEDED_ON_APPLICATION = 5; + MILESTONE_TYPE_INSTANCE_DELETED = 6; +} + +enum MilestoneFieldName { + MILESTONE_FIELD_NAME_UNSPECIFIED = 0; + MILESTONE_FIELD_NAME_TYPE = 1; + MILESTONE_FIELD_NAME_REACHED_DATE = 2; +} + +message Milestone { + // For the milestones, the standard details are not projected yet + reserved 1; + reserved "details"; + MilestoneType type = 2; + google.protobuf.Timestamp reached_date = 3; +} + +message MilestoneQuery { + oneof query { + IsReachedQuery is_reached_query = 1; + } +} + +message IsReachedQuery { + bool reached = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "only reached milestones"; + } + ]; +} From 385a55bd21a50fbf663a60e2b5472091a8ca324a Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 25 Oct 2023 13:42:00 +0200 Subject: [PATCH 28/48] feat: limit audit trail (#6744) * feat: enable limiting audit trail * support AddExclusiveQuery * fix invalid condition * register event mappers * fix NullDuration validity * test query side for limits * lint * acceptance test audit trail limit * fix acceptance test * translate limits not found * update tests * fix linting * add audit log retention to default instance * fix tests * update docs * remove todo * improve test name --- cmd/defaults.yaml | 11 +- cmd/start/start.go | 5 +- docs/docs/self-hosting/manage/production.md | 4 +- docs/docs/self-hosting/manage/quotas.md | 61 ---- .../docs/self-hosting/manage/usage_control.md | 117 +++++++ docs/sidebars.js | 2 +- e2e/config/localhost/zitadel.yaml | 11 - internal/api/grpc/admin/event.go | 2 +- internal/api/grpc/auth/server.go | 32 +- internal/api/grpc/auth/user.go | 2 +- internal/api/grpc/management/org.go | 2 +- internal/api/grpc/management/project.go | 4 +- .../grpc/management/project_application.go | 2 +- internal/api/grpc/management/server.go | 32 +- internal/api/grpc/management/user.go | 2 +- internal/api/grpc/system/limits.go | 32 ++ internal/api/grpc/system/limits_converter.go | 16 + .../grpc/system/limits_integration_test.go | 213 ++++++++++++ internal/command/command.go | 2 + internal/command/instance.go | 17 +- internal/command/limits.go | 105 ++++++ internal/command/limits_model.go | 73 ++++ internal/command/limits_test.go | 313 ++++++++++++++++++ internal/command/main_test.go | 2 + internal/database/type.go | 22 ++ internal/database/type_test.go | 57 ++++ internal/query/event.go | 56 ++-- internal/query/limits.go | 119 +++++++ internal/query/limits_test.go | 116 +++++++ internal/query/prepare_test.go | 12 + internal/query/projection/limits.go | 114 +++++++ internal/query/projection/limits_test.go | 96 ++++++ internal/query/projection/main_test.go | 2 + internal/query/projection/projection.go | 3 + internal/query/query.go | 5 + internal/query/quota_test.go | 23 +- internal/repository/limits/aggregate.go | 26 ++ internal/repository/limits/events.go | 86 +++++ internal/repository/limits/eventstore.go | 10 + internal/repository/quota/events.go | 8 - internal/static/i18n/bg.yaml | 3 + internal/static/i18n/de.yaml | 3 + internal/static/i18n/en.yaml | 3 + internal/static/i18n/es.yaml | 3 + internal/static/i18n/fr.yaml | 3 + internal/static/i18n/it.yaml | 3 + internal/static/i18n/ja.yaml | 3 + internal/static/i18n/mk.yaml | 3 + internal/static/i18n/pl.yaml | 3 + internal/static/i18n/pt.yaml | 3 + internal/static/i18n/zh.yaml | 3 + proto/zitadel/system.proto | 100 +++++- 52 files changed, 1778 insertions(+), 172 deletions(-) delete mode 100644 docs/docs/self-hosting/manage/quotas.md create mode 100644 docs/docs/self-hosting/manage/usage_control.md create mode 100644 internal/api/grpc/system/limits.go create mode 100644 internal/api/grpc/system/limits_converter.go create mode 100644 internal/api/grpc/system/limits_integration_test.go create mode 100644 internal/command/limits.go create mode 100644 internal/command/limits_model.go create mode 100644 internal/command/limits_test.go create mode 100644 internal/query/limits.go create mode 100644 internal/query/limits_test.go create mode 100644 internal/query/projection/limits.go create mode 100644 internal/query/projection/limits_test.go create mode 100644 internal/repository/limits/aggregate.go create mode 100644 internal/repository/limits/events.go create mode 100644 internal/repository/limits/eventstore.go diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 54eaaeeb6a..4baf021b9e 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -795,7 +795,11 @@ DefaultInstance: ButtonText: Login Features: - FeatureLoginDefaultOrg: true - + Limits: + # AuditLogRetention limits the number of events that can be queried via the events API by their age. + # A value of "0s" means that all events are available. + # If this value is set, it overwrites the system default unless it is not reset via the admin API. + AuditLogRetention: # ZITADEL_DEFAULTINSTANCE_LIMITS_AUDITLOGRETENTION Quotas: # Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist. # The following unit types are supported @@ -830,7 +834,10 @@ DefaultInstance: # # CallURL is called when a relative amount of the quota is used. # CallURL: "https://httpbin.org/post" -AuditLogRetention: 0s +# AuditLogRetention limits the number of events that can be queried via the events API by their age. +# A value of "0s" means that all events are available. +# If an audit log retention is set using an instance limit, it will overwrite the system default. +AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION InternalAuthZ: RolePermissionMappings: diff --git a/cmd/start/start.go b/cmd/start/start.go index 744e1f80ac..078075a17b 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -164,6 +164,7 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, + config.AuditLogRetention, config.SystemAPIUsers, ) if err != nil { @@ -364,10 +365,10 @@ func startAPIs( if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, config.SystemDefaults, config.ExternalSecure, keys.User, config.AuditLogRetention)); err != nil { return err } - if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil { + if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure)); err != nil { return err } - if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil { + if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure)); err != nil { return err } if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure))); err != nil { diff --git a/docs/docs/self-hosting/manage/production.md b/docs/docs/self-hosting/manage/production.md index 14117ebcce..b4ea7f3405 100644 --- a/docs/docs/self-hosting/manage/production.md +++ b/docs/docs/self-hosting/manage/production.md @@ -209,10 +209,10 @@ DefaultInstance: - Probably, you also want to [apply your custom branding](/guides/manage/customize/branding), [hook into certain events](/guides/manage/customize/behavior), [customize texts](/guides/manage/customize/texts) or [add metadata to your users](/guides/manage/customize/user-metadata). - If you want to automatically create ZITADEL resources, you can use the [ZITADEL Terraform Provider](/guides/manage/terraform/basics). -## Quotas +## Limits and Quotas If you host ZITADEL as a service, -you might want to [limit usage and/or execute tasks on certain usage units and levels](/self-hosting/manage/quotas). +you might want to [limit usage and/or execute tasks on certain usage units and levels](/self-hosting/manage/usage_control). ## Minimum system requirements diff --git a/docs/docs/self-hosting/manage/quotas.md b/docs/docs/self-hosting/manage/quotas.md deleted file mode 100644 index 23659242b4..0000000000 --- a/docs/docs/self-hosting/manage/quotas.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Usage Quotas in ZITADEL -sidebar_label: Usage Quotas ---- - -Quotas is an enterprise feature that is relevant if you want to host ZITADEL as a service. -It enables you to limit usage and/or register webhooks that trigger on configurable usage levels for certain units. -For example, you might want to report usage to an external billing tool and notify users when 80 percent of a quota is exhausted. -Quotas are currently supported [for the instance level only](/concepts/structure/instance). -Please refer to the [system API docs](/apis/resources/system) for detailed explanations about how to use the quotas feature. - -ZITADEL supports limiting authenticated requests and action run seconds - -## Authenticated Requests - -For using the quotas feature for authenticated requests you have to enable the database logstore for access logs in your ZITADEL configurations LogStore section: - -```yaml -LogStore: - Access: - Database: - # If enabled, all access logs are stored in the database table logstore.access - Enabled: false - # Logs that are older than the keep duration are cleaned up continuously - Keep: 2160h # 90 days - # CleanupInterval defines the time between cleanup iterations - CleanupInterval: 4h - # Debouncing enables to asynchronously emit log entries, so the normal execution performance is not impaired - # Log entries are held in-memory until one of the conditions MinFrequency or MaxBulkSize meets. - Debounce: - MinFrequency: 2m - MaxBulkSize: 100 -``` - -If a quota is configured to limit requests and the quotas amount is exhausted, all further requests are blocked except requests to the System API. -Also, a cookie is set, to make it easier to block further traffic before it reaches your ZITADEL runtime. - -## Action Run Seconds - -For using the quotas feature for action run seconds you have to enable the database logstore for execution logs in your ZITADEL configurations LogStore section: - -```yaml -LogStore: - Execution: - Database: - # If enabled, all action execution logs are stored in the database table logstore.execution - Enabled: false - # Logs that are older than the keep duration are cleaned up continuously - Keep: 2160h # 90 days - # CleanupInterval defines the time between cleanup iterations - CleanupInterval: 4h - # Debouncing enables to asynchronously emit log entries, so the normal execution performance is not impaired - # Log entries are held in-memory until one of the conditions MinFrequency or MaxBulkSize meets. - Debounce: - MinFrequency: 0s - MaxBulkSize: 0 -``` - -If a quota is configured to limit action run seconds and the quotas amount is exhausted, all further actions will fail immediately with a context timeout exceeded error. -The action that runs into the limit also fails with the context timeout exceeded error. - diff --git a/docs/docs/self-hosting/manage/usage_control.md b/docs/docs/self-hosting/manage/usage_control.md new file mode 100644 index 0000000000..af646e46e3 --- /dev/null +++ b/docs/docs/self-hosting/manage/usage_control.md @@ -0,0 +1,117 @@ +--- +title: Usage Control +sidebar_label: Usage Control +--- + +If you have a self-hosted ZITADEL environment, you can limit the usage of your [instances](/concepts/structure/instance). +For example, if you provide your customers [their own virtual instances](/concepts/structure/instance#multiple-virtual-instances) with access on their own domains, you can design a pricing model based on the usage of their instances. +The usage control features are currently limited to the instance level only. + +## Limit Audit Trails + +You can restrict the maximum age of events returned by the following APIs: + +- [Events Search](/apis/resources/admin/admin-service-list-events), See also the [Event API guide](guides/integrate/event-api) +- [My User History](/apis/resources/auth/auth-service-list-my-user-changes) +- [A Users History](/apis/resources/mgmt/management-service-list-user-changes) +- [An Applications History](/apis/resources/mgmt/management-service-list-app-changes) +- [An Organizations History](/apis/resources/mgmt/management-service-list-org-changes) +- [A Projects History](/apis/resources/mgmt/management-service-list-project-changes) +- [A Project Grants History](/apis/resources/mgmt/management-service-list-project-grant-changes) + +You can set a global default limit as well as a default limit [for new virtual instances](/concepts/structure/instance#multiple-virtual-instances) in the ZITADEL configuration. +The following snippets shows the defaults: + +```yaml +# AuditLogRetention limits the number of events that can be queried via the events API by their age. +# A value of "0s" means that all events are available. +# If an audit log retention is set using an instance limit, it will overwrite the system default. +AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION +DefaultInstance: + Limits: + # AuditLogRetention limits the number of events that can be queried via the events API by their age. + # A value of "0s" means that all events are available. + # If this value is set, it overwrites the system default unless it is not reset via the admin API. + AuditLogRetention: # ZITADEL_DEFAULTINSTANCE_LIMITS_AUDITLOGRETENTION +``` + +You can also set a limit for [a specific virtual instance](/concepts/structure/instance#multiple-virtual-instances) using the [system API](/category/apis/resources/system/limits). + +## Quotas + +Quotas enables you to limit usage and/or register webhooks that trigger on configurable usage levels for certain units. +For example, you might want to report usage to an external billing tool and notify users when 80 percent of a quota is exhausted. + +ZITADEL supports limiting authenticated requests and action run seconds with quotas. + +For using the quotas feature you have to activate it in your ZITADEL configurations *Quotas* section. +The following snippets shows the defaults: + +```yaml +Quotas: + Access: + # If enabled, authenticated requests are counted and potentially limited depending on the configured quota of the instance + Enabled: false # ZITADEL_QUOTAS_ACCESS_ENABLED + Debounce: + MinFrequency: 0s # ZITADEL_QUOTAS_ACCESS_DEBOUNCE_MINFREQUENCY + MaxBulkSize: 0 # ZITADEL_QUOTAS_ACCESS_DEBOUNCE_MAXBULKSIZE + ExhaustedCookieKey: "zitadel.quota.exhausted" # ZITADEL_QUOTAS_ACCESS_EXHAUSTEDCOOKIEKEY + ExhaustedCookieMaxAge: "300s" # ZITADEL_QUOTAS_ACCESS_EXHAUSTEDCOOKIEMAXAGE + Execution: + # If enabled, all action executions are counted and potentially limited depending on the configured quota of the instance + Enabled: false # ZITADEL_QUOTAS_EXECUTION_DATABASE_ENABLED + Debounce: + MinFrequency: 0s # ZITADEL_QUOTAS_EXECUTION_DEBOUNCE_MINFREQUENCY + MaxBulkSize: 0 # ZITADEL_QUOTAS_EXECUTION_DEBOUNCE_MAXBULKSIZE +``` + +Once you have activated the quotas feature, you can configure quotas [for your virtual instances](/concepts/structure/instance#multiple-virtual-instances) using the [system API](/category/apis/resources/system/quotas) or the *DefaultInstances.Quotas* section. +The following snippets shows the defaults: + +```yaml +DefaultInstance: + Quotas: + # Items take a slice of quota configurations, whereas, for each unit type and instance, one or zero quotas may exist. + # The following unit types are supported + + # "requests.all.authenticated" + # The sum of all requests to the ZITADEL API with an authorization header, + # excluding the following exceptions + # - Calls to the System API + # - Calls that cause internal server errors + # - Failed authorizations + # - Requests after the quota already exceeded + + # "actions.all.runs.seconds" + # The sum of all actions run durations in seconds + Items: +# - Unit: "requests.all.authenticated" +# # From defines the starting time from which the current quota period is calculated. +# # This is relevant for querying the current usage. +# From: "2023-01-01T00:00:00Z" +# # ResetInterval defines the quota periods duration +# ResetInterval: 720h # 30 days +# # Amount defines the number of units for this quota +# Amount: 25000 +# # Limit defines whether ZITADEL should block further usage when the configured amount is used +# Limit: false +# # Notifications are emitted by ZITADEL when certain quota percentages are reached +# Notifications: +# # Percent defines the relative amount of used units, after which a notification should be emitted. +# - Percent: 100 +# # Repeat defines, whether a notification should be emitted each time when a multitude of the configured Percent is used. +# Repeat: true +# # CallURL is called when a relative amount of the quota is used. +# CallURL: "https://httpbin.org/post" +``` + +### Exhausted Authenticated Requests + +If a quota is configured to limit requests and the quotas amount is exhausted, all further requests are blocked except requests to the System API. +Also, a cookie is set, to make it easier to block further traffic before it reaches your ZITADEL runtime. + +### Exhausted Action Run Seconds + +If a quota is configured to limit action run seconds and the quotas amount is exhausted, all further actions will fail immediately with a context timeout exceeded error. +The action that runs into the limit also fails with the context timeout exceeded error. + diff --git a/docs/sidebars.js b/docs/sidebars.js index a2ba179c80..10c5e87961 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -664,7 +664,7 @@ module.exports = { "self-hosting/manage/tls_modes", "self-hosting/manage/database/database", "self-hosting/manage/updating_scaling", - "self-hosting/manage/quotas" + "self-hosting/manage/usage_control" ], }, ], diff --git a/e2e/config/localhost/zitadel.yaml b/e2e/config/localhost/zitadel.yaml index cfafaa637f..aa051ac112 100644 --- a/e2e/config/localhost/zitadel.yaml +++ b/e2e/config/localhost/zitadel.yaml @@ -40,17 +40,6 @@ Quotas: DefaultInstance: LoginPolicy: MfaInitSkipLifetime: "0" - Quotas: - Items: - - Unit: "actions.all.runs.seconds" - From: "2023-01-01T00:00:00Z" - ResetInterval: 5m - Amount: 20 - Limit: false - Notifications: - - Percent: 100 - Repeat: true - CallURL: "https://httpbin.org/post" SystemAPIUsers: - cypress: diff --git a/internal/api/grpc/admin/event.go b/internal/api/grpc/admin/event.go index 2c7a2fe63f..434290d8ea 100644 --- a/internal/api/grpc/admin/event.go +++ b/internal/api/grpc/admin/event.go @@ -17,7 +17,7 @@ func (s *Server) ListEvents(ctx context.Context, in *admin_pb.ListEventsRequest) if err != nil { return nil, err } - events, err := s.query.SearchEvents(ctx, filter, s.auditLogRetention) + events, err := s.query.SearchEvents(ctx, filter) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index 16fb66022f..015c8ce83f 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -2,7 +2,6 @@ package auth import ( "context" - "time" "google.golang.org/grpc" @@ -26,14 +25,13 @@ const ( type Server struct { auth.UnimplementedAuthServiceServer - command *command.Commands - query *query.Queries - repo repository.Repository - defaults systemdefaults.SystemDefaults - assetsAPIDomain func(context.Context) string - userCodeAlg crypto.EncryptionAlgorithm - externalSecure bool - auditLogRetention time.Duration + command *command.Commands + query *query.Queries + repo repository.Repository + defaults systemdefaults.SystemDefaults + assetsAPIDomain func(context.Context) string + userCodeAlg crypto.EncryptionAlgorithm + externalSecure bool } type Config struct { @@ -46,17 +44,15 @@ func CreateServer(command *command.Commands, defaults systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, externalSecure bool, - auditLogRetention time.Duration, ) *Server { return &Server{ - command: command, - query: query, - repo: authRepo, - defaults: defaults, - assetsAPIDomain: assets.AssetAPI(externalSecure), - userCodeAlg: userCodeAlg, - externalSecure: externalSecure, - auditLogRetention: auditLogRetention, + command: command, + query: query, + repo: authRepo, + defaults: defaults, + assetsAPIDomain: assets.AssetAPI(externalSecure), + userCodeAlg: userCodeAlg, + externalSecure: externalSecure, } } diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 11ecb95cf8..87a146efe6 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -84,7 +84,7 @@ func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserC query.OrderAsc() } - changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention) + changes, err := s.query.SearchEvents(ctx, query) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index 5879cd9718..157537260a 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -63,7 +63,7 @@ func (s *Server) ListOrgChanges(ctx context.Context, req *mgmt_pb.ListOrgChanges query.OrderAsc() } - response, err := s.query.SearchEvents(ctx, query, s.auditLogRetention) + response, err := s.query.SearchEvents(ctx, query) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index 9d02dc7a58..fc43227c95 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -87,7 +87,7 @@ func (s *Server) ListProjectGrantChanges(ctx context.Context, req *mgmt_pb.ListP query.OrderAsc() } - changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention) + changes, err := s.query.SearchEvents(ctx, query) if err != nil { return nil, err } @@ -166,7 +166,7 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec query.OrderAsc() } - changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention) + changes, err := s.query.SearchEvents(ctx, query) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index ff053089ed..ef18563c1e 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -70,7 +70,7 @@ func (s *Server) ListAppChanges(ctx context.Context, req *mgmt_pb.ListAppChanges query.OrderAsc() } - changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention) + changes, err := s.query.SearchEvents(ctx, query) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index 12413e1bc8..b0f9879278 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -2,7 +2,6 @@ package management import ( "context" - "time" "google.golang.org/grpc" @@ -24,14 +23,13 @@ var _ management.ManagementServiceServer = (*Server)(nil) type Server struct { management.UnimplementedManagementServiceServer - command *command.Commands - query *query.Queries - systemDefaults systemdefaults.SystemDefaults - assetAPIPrefix func(context.Context) string - passwordHashAlg crypto.HashAlgorithm - userCodeAlg crypto.EncryptionAlgorithm - externalSecure bool - auditLogRetention time.Duration + command *command.Commands + query *query.Queries + systemDefaults systemdefaults.SystemDefaults + assetAPIPrefix func(context.Context) string + passwordHashAlg crypto.HashAlgorithm + userCodeAlg crypto.EncryptionAlgorithm + externalSecure bool } func CreateServer( @@ -40,17 +38,15 @@ func CreateServer( sd systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, externalSecure bool, - auditLogRetention time.Duration, ) *Server { return &Server{ - command: command, - query: query, - systemDefaults: sd, - assetAPIPrefix: assets.AssetAPI(externalSecure), - passwordHashAlg: crypto.NewBCrypt(sd.SecretGenerators.PasswordSaltCost), - userCodeAlg: userCodeAlg, - externalSecure: externalSecure, - auditLogRetention: auditLogRetention, + command: command, + query: query, + systemDefaults: sd, + assetAPIPrefix: assets.AssetAPI(externalSecure), + passwordHashAlg: crypto.NewBCrypt(sd.SecretGenerators.PasswordSaltCost), + userCodeAlg: userCodeAlg, + externalSecure: externalSecure, } } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index c61832169a..33b5606141 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -109,7 +109,7 @@ func (s *Server) ListUserChanges(ctx context.Context, req *mgmt_pb.ListUserChang query.OrderAsc() } - changes, err := s.query.SearchEvents(ctx, query, s.auditLogRetention) + changes, err := s.query.SearchEvents(ctx, query) if err != nil { return nil, err } diff --git a/internal/api/grpc/system/limits.go b/internal/api/grpc/system/limits.go new file mode 100644 index 0000000000..f41ddb2231 --- /dev/null +++ b/internal/api/grpc/system/limits.go @@ -0,0 +1,32 @@ +package system + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func (s *Server) SetLimits(ctx context.Context, req *system.SetLimitsRequest) (*system.SetLimitsResponse, error) { + details, err := s.command.SetLimits( + ctx, + req.GetInstanceId(), + instanceLimitsPbToCommand(req), + ) + if err != nil { + return nil, err + } + return &system.SetLimitsResponse{ + Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner), + }, nil +} + +func (s *Server) ResetLimits(ctx context.Context, req *system.ResetLimitsRequest) (*system.ResetLimitsResponse, error) { + details, err := s.command.ResetLimits(ctx, req.GetInstanceId()) + if err != nil { + return nil, err + } + return &system.ResetLimitsResponse{ + Details: object.ChangeToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner), + }, nil +} diff --git a/internal/api/grpc/system/limits_converter.go b/internal/api/grpc/system/limits_converter.go new file mode 100644 index 0000000000..de7f330475 --- /dev/null +++ b/internal/api/grpc/system/limits_converter.go @@ -0,0 +1,16 @@ +package system + +import ( + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func instanceLimitsPbToCommand(req *system.SetLimitsRequest) *command.SetLimits { + var setLimits = new(command.SetLimits) + if req.AuditLogRetention != nil { + setLimits.AuditLogRetention = gu.Ptr(req.AuditLogRetention.AsDuration()) + } + return setLimits +} diff --git a/internal/api/grpc/system/limits_integration_test.go b/internal/api/grpc/system/limits_integration_test.go new file mode 100644 index 0000000000..e2480d0c0c --- /dev/null +++ b/internal/api/grpc/system/limits_integration_test.go @@ -0,0 +1,213 @@ +//go:build integration + +package system_test + +import ( + "context" + "math/rand" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/pkg/grpc/admin" + "github.com/zitadel/zitadel/pkg/grpc/auth" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func TestServer_Limits_AuditLogRetention(t *testing.T) { + _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(CTX, SystemCTX) + userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t) + beforeTime := time.Now() + zeroCounts := &eventCounts{} + seededCount := requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) { + counts.assertAll(t, c, "seeded events are > 0", assert.Greater, zeroCounts) + }, "wait for seeded event assertions to pass") + produceEvents(iamOwnerCtx, t, userID, appID, projectID, projectGrantID) + addedCount := requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) { + counts.assertAll(t, c, "added events are > seeded events", assert.Greater, seededCount) + }, "wait for added event assertions to pass") + _, err := Tester.Client.System.SetLimits(SystemCTX, &system.SetLimitsRequest{ + InstanceId: instanceID, + AuditLogRetention: durationpb.New(time.Now().Sub(beforeTime)), + }) + require.NoError(t, err) + requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) { + counts.assertAll(t, c, "limited events < added events", assert.Less, addedCount) + counts.assertAll(t, c, "limited events > 0", assert.Greater, zeroCounts) + }, "wait for limited event assertions to pass") + _, err = Tester.Client.System.ResetLimits(SystemCTX, &system.ResetLimitsRequest{ + InstanceId: instanceID, + }) + require.NoError(t, err) + requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) { + counts.assertAll(t, c, "with reset limit, added events are > seeded events", assert.Greater, seededCount) + }, "wait for reset event assertions to pass") +} + +func requireEventually( + t *testing.T, + ctx context.Context, + userID, projectID, appID, projectGrantID string, + assertCounts func(assert.TestingT, *eventCounts), + msg string, +) (counts *eventCounts) { + countTimeout := 30 * time.Second + assertTimeout := countTimeout + time.Second + countCtx, cancel := context.WithTimeout(ctx, countTimeout) + defer cancel() + require.EventuallyWithT(t, func(c *assert.CollectT) { + counts = countEvents(countCtx, t, userID, projectID, appID, projectGrantID) + assertCounts(c, counts) + }, assertTimeout, time.Second, msg) + return counts +} + +var runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randomString(resourceType string, n int) string { + b := make([]rune, n) + for i := range b { + b[i] = runes[rand.Intn(len(runes))] + } + return "test" + resourceType + "-" + string(b) +} + +func seedObjects(ctx context.Context, t *testing.T) (string, string, string, string) { + t.Helper() + project, err := Tester.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ + Name: randomString("project", 5), + }) + require.NoError(t, err) + app, err := Tester.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ + Name: randomString("app", 5), + ProjectId: project.GetId(), + }) + org, err := Tester.Client.Mgmt.AddOrg(ctx, &management.AddOrgRequest{ + Name: randomString("org", 5), + }) + require.NoError(t, err) + role := randomString("role", 5) + require.NoError(t, err) + _, err = Tester.Client.Mgmt.AddProjectRole(ctx, &management.AddProjectRoleRequest{ + ProjectId: project.GetId(), + RoleKey: role, + DisplayName: role, + }) + require.NoError(t, err) + projectGrant, err := Tester.Client.Mgmt.AddProjectGrant(ctx, &management.AddProjectGrantRequest{ + ProjectId: project.GetId(), + GrantedOrgId: org.GetId(), + RoleKeys: []string{role}, + }) + require.NoError(t, err) + user, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{}) + require.NoError(t, err) + userID := user.GetUser().GetId() + requireUserEvent(ctx, t, userID) + return userID, project.GetId(), app.GetAppId(), projectGrant.GetGrantId() +} + +func produceEvents(ctx context.Context, t *testing.T, machineID, appID, projectID, grantID string) { + t.Helper() + _, err := Tester.Client.Mgmt.UpdateOrg(ctx, &management.UpdateOrgRequest{ + Name: randomString("org", 5), + }) + require.NoError(t, err) + _, err = Tester.Client.Mgmt.UpdateProject(ctx, &management.UpdateProjectRequest{ + Id: projectID, + Name: randomString("project", 5), + }) + require.NoError(t, err) + _, err = Tester.Client.Mgmt.UpdateApp(ctx, &management.UpdateAppRequest{ + AppId: appID, + ProjectId: projectID, + Name: randomString("app", 5), + }) + require.NoError(t, err) + requireUserEvent(ctx, t, machineID) + _, err = Tester.Client.Mgmt.UpdateProjectGrant(ctx, &management.UpdateProjectGrantRequest{ + ProjectId: projectID, + GrantId: grantID, + }) + require.NoError(t, err) +} + +func requireUserEvent(ctx context.Context, t *testing.T, machineID string) { + _, err := Tester.Client.Mgmt.UpdateMachine(ctx, &management.UpdateMachineRequest{ + UserId: machineID, + Name: randomString("machine", 5), + }) + require.NoError(t, err) +} + +type eventCounts struct { + all, myUser, aUser, grant, project, app, org int +} + +func (e *eventCounts) assertAll(t *testing.T, c assert.TestingT, name string, compare assert.ComparisonAssertionFunc, than *eventCounts) { + t.Run(name, func(t *testing.T) { + compare(c, e.all, than.all, "ListEvents") + compare(c, e.myUser, than.myUser, "ListMyUserChanges") + compare(c, e.aUser, than.aUser, "ListUserChanges") + compare(c, e.grant, than.grant, "ListProjectGrantChanges") + compare(c, e.project, than.project, "ListProjectChanges") + compare(c, e.app, than.app, "ListAppChanges") + compare(c, e.org, than.org, "ListOrgChanges") + }) +} + +func countEvents(ctx context.Context, t *testing.T, userID, projectID, appID, grantID string) *eventCounts { + t.Helper() + counts := new(eventCounts) + var wg sync.WaitGroup + wg.Add(7) + go func() { + defer wg.Done() + result, err := Tester.Client.Admin.ListEvents(ctx, &admin.ListEventsRequest{}) + require.NoError(t, err) + counts.all = len(result.GetEvents()) + }() + go func() { + defer wg.Done() + result, err := Tester.Client.Auth.ListMyUserChanges(ctx, &auth.ListMyUserChangesRequest{}) + require.NoError(t, err) + counts.myUser = len(result.GetResult()) + }() + go func() { + defer wg.Done() + result, err := Tester.Client.Mgmt.ListUserChanges(ctx, &management.ListUserChangesRequest{UserId: userID}) + require.NoError(t, err) + counts.aUser = len(result.GetResult()) + }() + go func() { + defer wg.Done() + result, err := Tester.Client.Mgmt.ListAppChanges(ctx, &management.ListAppChangesRequest{ProjectId: projectID, AppId: appID}) + require.NoError(t, err) + counts.app = len(result.GetResult()) + }() + go func() { + defer wg.Done() + result, err := Tester.Client.Mgmt.ListOrgChanges(ctx, &management.ListOrgChangesRequest{}) + require.NoError(t, err) + counts.org = len(result.GetResult()) + }() + go func() { + defer wg.Done() + result, err := Tester.Client.Mgmt.ListProjectChanges(ctx, &management.ListProjectChangesRequest{ProjectId: projectID}) + require.NoError(t, err) + counts.project = len(result.GetResult()) + }() + go func() { + defer wg.Done() + result, err := Tester.Client.Mgmt.ListProjectGrantChanges(ctx, &management.ListProjectGrantChangesRequest{ProjectId: projectID, GrantId: grantID}) + require.NoError(t, err) + counts.grant = len(result.GetResult()) + }() + wg.Wait() + return counts +} diff --git a/internal/command/command.go b/internal/command/command.go index 6288bbd234..5589965a39 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -26,6 +26,7 @@ import ( "github.com/zitadel/zitadel/internal/repository/idpintent" instance_repo "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/keypair" + "github.com/zitadel/zitadel/internal/repository/limits" "github.com/zitadel/zitadel/internal/repository/milestone" "github.com/zitadel/zitadel/internal/repository/oidcsession" "github.com/zitadel/zitadel/internal/repository/org" @@ -150,6 +151,7 @@ func StartCommands( keypair.RegisterEventMappers(repo.eventstore) action.RegisterEventMappers(repo.eventstore) quota.RegisterEventMappers(repo.eventstore) + limits.RegisterEventMappers(repo.eventstore) session.RegisterEventMappers(repo.eventstore) idpintent.RegisterEventMappers(repo.eventstore) authrequest.RegisterEventMappers(repo.eventstore) diff --git a/internal/command/instance.go b/internal/command/instance.go index f82252f5a3..4c5bb2faa5 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -17,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/notification/channels/smtp" "github.com/zitadel/zitadel/internal/repository/feature" "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/limits" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/quota" @@ -114,6 +115,9 @@ type InstanceSetup struct { Items []*SetQuota } Features map[domain.Feature]any + Limits *struct { + AuditLogRetention *time.Duration + } } type SecretGenerators struct { @@ -135,6 +139,7 @@ type ZitadelConfig struct { adminAppID string authAppID string consoleAppID string + limitsID string } func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) { @@ -159,7 +164,10 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) { } s.zitadel.consoleAppID, err = idGenerator.Next() - + if err != nil { + return err + } + s.zitadel.limitsID, err = idGenerator.Next() return err } @@ -190,6 +198,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str orgAgg := org.NewAggregate(orgID) userAgg := user.NewAggregate(userID, orgID) projectAgg := project.NewAggregate(setup.zitadel.projectID, orgID) + limitsAgg := limits.NewAggregate(setup.zitadel.limitsID, instanceID, instanceID) validations := []preparation.Validation{ prepareAddInstance(instanceAgg, setup.InstanceName, setup.DefaultLanguage), @@ -441,6 +450,12 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str } } + if setup.Limits != nil { + validations = append(validations, c.SetLimitsCommand(limitsAgg, &limitsWriteModel{}, &SetLimits{ + AuditLogRetention: setup.Limits.AuditLogRetention, + })) + } + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { return "", "", nil, nil, err diff --git a/internal/command/limits.go b/internal/command/limits.go new file mode 100644 index 0000000000..5c7cdd8ee5 --- /dev/null +++ b/internal/command/limits.go @@ -0,0 +1,105 @@ +package command + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/limits" +) + +type SetLimits struct { + AuditLogRetention *time.Duration `json:"AuditLogRetention,omitempty"` +} + +// SetLimits creates new limits or updates existing limits. +func (c *Commands) SetLimits( + ctx context.Context, + resourceOwner string, + setLimits *SetLimits, +) (*domain.ObjectDetails, error) { + instanceId := authz.GetInstance(ctx).InstanceID() + wm, err := c.getLimitsWriteModel(ctx, instanceId, resourceOwner) + if err != nil { + return nil, err + } + aggregateId := wm.AggregateID + if aggregateId == "" { + aggregateId, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + if err != nil { + return nil, err + } + createCmds, err := c.SetLimitsCommand(limits.NewAggregate(aggregateId, instanceId, resourceOwner), wm, setLimits)() + if err != nil { + return nil, err + } + cmds, err := createCmds(ctx, nil) + if len(cmds) > 0 { + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + err = AppendAndReduce(wm, events...) + if err != nil { + return nil, err + } + } + return writeModelToObjectDetails(&wm.WriteModel), nil +} + +func (c *Commands) ResetLimits(ctx context.Context, resourceOwner string) (*domain.ObjectDetails, error) { + instanceId := authz.GetInstance(ctx).InstanceID() + wm, err := c.getLimitsWriteModel(ctx, instanceId, resourceOwner) + if err != nil { + return nil, err + } + if wm.AggregateID == "" { + return nil, errors.ThrowNotFound(nil, "COMMAND-9JToT", "Errors.Limits.NotFound") + } + aggregate := limits.NewAggregate(wm.AggregateID, instanceId, resourceOwner) + events := []eventstore.Command{limits.NewResetEvent(ctx, &aggregate.Aggregate)} + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(wm, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&wm.WriteModel), nil +} + +func (c *Commands) getLimitsWriteModel(ctx context.Context, instanceId, resourceOwner string) (*limitsWriteModel, error) { + wm := newLimitsWriteModel(instanceId, resourceOwner) + return wm, c.eventstore.FilterToQueryReducer(ctx, wm) +} + +func (c *Commands) SetLimitsCommand(a *limits.Aggregate, wm *limitsWriteModel, setLimits *SetLimits) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if setLimits == nil || setLimits.AuditLogRetention == nil { + return nil, errors.ThrowInvalidArgument(nil, "COMMAND-4M9vs", "Errors.Limits.NoneSpecified") + } + return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + changes := wm.NewChanges(setLimits) + if len(changes) == 0 { + return nil, nil + } + return []eventstore.Command{limits.NewSetEvent( + eventstore.NewBaseEventForPush( + ctx, + &a.Aggregate, + limits.SetEventType, + ), + changes..., + )}, nil + }, nil + } +} diff --git a/internal/command/limits_model.go b/internal/command/limits_model.go new file mode 100644 index 0000000000..528c0873fa --- /dev/null +++ b/internal/command/limits_model.go @@ -0,0 +1,73 @@ +package command + +import ( + "time" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/limits" +) + +type limitsWriteModel struct { + eventstore.WriteModel + rollingAggregateID string + auditLogRetention *time.Duration +} + +// newLimitsWriteModel aggregateId is filled by reducing unit matching events +func newLimitsWriteModel(instanceId, resourceOwner string) *limitsWriteModel { + return &limitsWriteModel{ + WriteModel: eventstore.WriteModel{ + InstanceID: instanceId, + ResourceOwner: resourceOwner, + }, + } +} + +func (wm *limitsWriteModel) Query() *eventstore.SearchQueryBuilder { + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). + InstanceID(wm.InstanceID). + AddQuery(). + AggregateTypes(limits.AggregateType). + EventTypes( + limits.SetEventType, + limits.ResetEventType, + ) + + return query.Builder() +} + +func (wm *limitsWriteModel) Reduce() error { + for _, event := range wm.Events { + wm.ChangeDate = event.CreatedAt() + switch e := event.(type) { + case *limits.SetEvent: + wm.rollingAggregateID = e.Aggregate().ID + if e.AuditLogRetention != nil { + wm.auditLogRetention = e.AuditLogRetention + } + case *limits.ResetEvent: + wm.rollingAggregateID = "" + wm.auditLogRetention = nil + } + } + if err := wm.WriteModel.Reduce(); err != nil { + return err + } + // wm.WriteModel.Reduce() sets the aggregateID to the first event's aggregateID, but we need the last one + wm.AggregateID = wm.rollingAggregateID + return nil +} + +// NewChanges returns all changes that need to be applied to the aggregate. +// nil properties in setLimits are ignored +func (wm *limitsWriteModel) NewChanges(setLimits *SetLimits) (changes []limits.LimitsChange) { + if setLimits == nil { + return nil + } + changes = make([]limits.LimitsChange, 0, 1) + if setLimits.AuditLogRetention != nil && (wm.auditLogRetention == nil || *wm.auditLogRetention != *setLimits.AuditLogRetention) { + changes = append(changes, limits.ChangeAuditLogRetention(setLimits.AuditLogRetention)) + } + return changes +} diff --git a/internal/command/limits_test.go b/internal/command/limits_test.go new file mode 100644 index 0000000000..1b315ca130 --- /dev/null +++ b/internal/command/limits_test.go @@ -0,0 +1,313 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + caos_errors "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/id" + id_mock "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/limits" +) + +func TestLimits_SetLimits(t *testing.T) { + type fields func(*testing.T) (*eventstore.Eventstore, id.Generator) + type args struct { + ctx context.Context + resourceOwner string + setLimits *SetLimits + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "create limits, ok", + fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) { + return eventstoreExpect( + t, + expectFilter(), + expectPush( + eventFromEventPusherWithInstanceID( + "instance1", + limits.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + limits.SetEventType, + ), + limits.ChangeAuditLogRetention(gu.Ptr(time.Hour)), + ), + ), + ), + ), + id_mock.NewIDGeneratorExpectIDs(t, "limits1") + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + resourceOwner: "instance1", + setLimits: &SetLimits{ + AuditLogRetention: gu.Ptr(time.Hour), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + }, + { + name: "update limits, ok", + fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) { + return eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + limits.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + limits.SetEventType, + ), + limits.ChangeAuditLogRetention(gu.Ptr(time.Minute)), + ), + ), + ), + expectPush( + eventFromEventPusherWithInstanceID( + "instance1", + limits.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + limits.SetEventType, + ), + limits.ChangeAuditLogRetention(gu.Ptr(time.Hour)), + ), + ), + ), + ), + nil + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + resourceOwner: "instance1", + setLimits: &SetLimits{ + AuditLogRetention: gu.Ptr(time.Hour), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + }, + { + name: "set limits after resetting limits, ok", + fields: func(*testing.T) (*eventstore.Eventstore, id.Generator) { + return eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + limits.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + limits.SetEventType, + ), + limits.ChangeAuditLogRetention(gu.Ptr(time.Hour)), + ), + ), + eventFromEventPusher( + limits.NewResetEvent( + context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + ), + ), + ), + expectPush( + eventFromEventPusherWithInstanceID( + "instance1", + limits.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &limits.NewAggregate("limits2", "instance1", "instance1").Aggregate, + limits.SetEventType, + ), + limits.ChangeAuditLogRetention(gu.Ptr(time.Hour)), + ), + ), + ), + ), + id_mock.NewIDGeneratorExpectIDs(t, "limits2") + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + resourceOwner: "instance1", + setLimits: &SetLimits{ + AuditLogRetention: gu.Ptr(time.Hour), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := new(Commands) + r.eventstore, r.idGenerator = tt.fields(t) + got, err := r.SetLimits(tt.args.ctx, tt.args.resourceOwner, tt.args.setLimits) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} + +func TestLimits_ResetLimits(t *testing.T) { + type fields func(*testing.T) *eventstore.Eventstore + type args struct { + ctx context.Context + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "not found", + fields: func(tt *testing.T) *eventstore.Eventstore { + return eventstoreExpect( + tt, + expectFilter(), + ) + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + resourceOwner: "instance1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowNotFound(nil, "COMMAND-9JToT", "Errors.Limits.NotFound")) + }, + }, + }, + { + name: "already removed", + fields: func(tt *testing.T) *eventstore.Eventstore { + return eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + limits.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + limits.SetEventType, + ), + limits.ChangeAuditLogRetention(gu.Ptr(time.Hour)), + ), + ), + eventFromEventPusher( + limits.NewResetEvent(context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + ), + ), + ), + ) + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + resourceOwner: "instance1", + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, caos_errors.ThrowNotFound(nil, "COMMAND-9JToT", "Errors.Limits.NotFound")) + }, + }, + }, + { + name: "reset limits, ok", + fields: func(tt *testing.T) *eventstore.Eventstore { + return eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + limits.NewSetEvent( + eventstore.NewBaseEventForPush( + context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + limits.SetEventType, + ), + limits.ChangeAuditLogRetention(gu.Ptr(time.Hour)), + ), + ), + ), + expectPush( + eventFromEventPusherWithInstanceID( + "instance1", + limits.NewResetEvent(context.Background(), + &limits.NewAggregate("limits1", "instance1", "instance1").Aggregate, + ), + ), + ), + ) + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + resourceOwner: "instance1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields(t), + } + got, err := r.ResetLimits(tt.args.ctx, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/main_test.go b/internal/command/main_test.go index c830409489..5458811eb7 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -24,6 +24,7 @@ import ( "github.com/zitadel/zitadel/internal/repository/idpintent" iam_repo "github.com/zitadel/zitadel/internal/repository/instance" key_repo "github.com/zitadel/zitadel/internal/repository/keypair" + "github.com/zitadel/zitadel/internal/repository/limits" "github.com/zitadel/zitadel/internal/repository/oidcsession" "github.com/zitadel/zitadel/internal/repository/org" proj_repo "github.com/zitadel/zitadel/internal/repository/project" @@ -58,6 +59,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore { authrequest.RegisterEventMappers(es) oidcsession.RegisterEventMappers(es) quota_repo.RegisterEventMappers(es) + limits.RegisterEventMappers(es) feature.RegisterEventMappers(es) return es } diff --git a/internal/database/type.go b/internal/database/type.go index 2608dc4f40..1bd5436419 100644 --- a/internal/database/type.go +++ b/internal/database/type.go @@ -103,3 +103,25 @@ func (d *Duration) Scan(src any) error { *d = Duration(time.Duration(interval.Microseconds*1000) + time.Duration(interval.Days)*24*time.Hour + time.Duration(interval.Months)*30*24*time.Hour) return nil } + +// NullDuration can be used for NULL intervals. +// If Valid is false, the scanned value was NULL +// This behavior is similar to [database/sql.NullString] +type NullDuration struct { + Valid bool + Duration time.Duration +} + +// Scan implements the [database/sql.Scanner] interface. +func (d *NullDuration) Scan(src any) error { + if src == nil { + d.Duration, d.Valid = 0, false + return nil + } + duration := new(Duration) + if err := duration.Scan(src); err != nil { + return err + } + d.Duration, d.Valid = time.Duration(*duration), true + return nil +} diff --git a/internal/database/type_test.go b/internal/database/type_test.go index 9ac777dab6..f0c308a184 100644 --- a/internal/database/type_test.go +++ b/internal/database/type_test.go @@ -3,6 +3,7 @@ package database import ( "database/sql/driver" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -118,6 +119,62 @@ func TestMap_Value(t *testing.T) { } } +func TestNullDuration_Scan(t *testing.T) { + type args struct { + src any + } + type res struct { + want NullDuration + err bool + } + type testCase struct { + name string + args args + res res + } + tests := []testCase{ + { + "invalid", + args{src: "invalid"}, + res{ + want: NullDuration{ + Valid: false, + }, + err: true, + }, + }, + { + "null", + args{src: nil}, + res{ + want: NullDuration{ + Valid: false, + }, + err: false, + }, + }, + { + "valid", + args{src: "1:0:0"}, + res{ + want: NullDuration{ + Valid: true, + Duration: time.Hour, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := new(NullDuration) + if err := d.Scan(tt.args.src); (err != nil) != tt.res.err { + t.Errorf("Scan() error = %v, wantErr %v", err, tt.res.err) + } + assert.Equal(t, tt.res.want, *d) + }) + } +} + func TestArray_ScanInt32(t *testing.T) { type args struct { src any diff --git a/internal/query/event.go b/internal/query/event.go index 63ffa3fe4a..c387c045a8 100644 --- a/internal/query/event.go +++ b/internal/query/event.go @@ -4,7 +4,9 @@ import ( "context" "time" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -26,33 +28,45 @@ type EventEditor struct { AvatarKey string } -func (q *Queries) SearchEvents(ctx context.Context, query *eventstore.SearchQueryBuilder, auditLogRetention time.Duration) (_ []*Event, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - events, err := q.eventstore.Filter(ctx, query.AllowTimeTravel()) - if err != nil { - return nil, err - } - - if auditLogRetention != 0 { - events = filterAuditLogRetention(ctx, events, auditLogRetention) - } - - return q.convertEvents(ctx, events), nil +type eventsReducer struct { + ctx context.Context + q *Queries + events []*Event } -func filterAuditLogRetention(ctx context.Context, events []eventstore.Event, auditLogRetention time.Duration) []eventstore.Event { +func (r *eventsReducer) AppendEvents(events ...eventstore.Event) { + r.events = append(r.events, r.q.convertEvents(r.ctx, events)...) +} + +func (r *eventsReducer) Reduce() error { return nil } + +func (q *Queries) SearchEvents(ctx context.Context, query *eventstore.SearchQueryBuilder) (_ []*Event, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + auditLogRetention := q.defaultAuditLogRetention + instanceLimits, err := q.Limits(ctx, authz.GetInstance(ctx).InstanceID()) + if err != nil && !errors.IsNotFound(err) { + return nil, err + } + if instanceLimits != nil && instanceLimits.AuditLogRetention != nil { + auditLogRetention = *instanceLimits.AuditLogRetention + } + if auditLogRetention != 0 { + query = filterAuditLogRetention(ctx, auditLogRetention, query) + } + reducer := &eventsReducer{ctx: ctx, q: q} + if err = q.eventstore.FilterToReducer(ctx, query, reducer); err != nil { + return nil, err + } + return reducer.events, nil +} + +func filterAuditLogRetention(ctx context.Context, auditLogRetention time.Duration, builder *eventstore.SearchQueryBuilder) *eventstore.SearchQueryBuilder { callTime := call.FromContext(ctx) if callTime.IsZero() { callTime = time.Now() } - filteredEvents := make([]eventstore.Event, 0, len(events)) - for _, event := range events { - if event.CreatedAt().After(callTime.Add(-auditLogRetention)) { - filteredEvents = append(filteredEvents, event) - } - } - return filteredEvents + return builder.CreationDateAfter(callTime.Add(-auditLogRetention)) } func (q *Queries) SearchEventTypes(ctx context.Context) []string { diff --git a/internal/query/limits.go b/internal/query/limits.go new file mode 100644 index 0000000000..cf9635026d --- /dev/null +++ b/internal/query/limits.go @@ -0,0 +1,119 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/call" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +var ( + limitSettingsTable = table{ + name: projection.LimitsProjectionTable, + instanceIDCol: projection.LimitsColumnInstanceID, + } + LimitsColumnAggregateID = Column{ + name: projection.LimitsColumnAggregateID, + table: limitSettingsTable, + } + LimitsColumnCreationDate = Column{ + name: projection.LimitsColumnCreationDate, + table: limitSettingsTable, + } + LimitsColumnChangeDate = Column{ + name: projection.LimitsColumnChangeDate, + table: limitSettingsTable, + } + LimitsColumnResourceOwner = Column{ + name: projection.LimitsColumnResourceOwner, + table: limitSettingsTable, + } + LimitsColumnInstanceID = Column{ + name: projection.LimitsColumnInstanceID, + table: limitSettingsTable, + } + LimitsColumnSequence = Column{ + name: projection.LimitsColumnSequence, + table: limitSettingsTable, + } + LimitsColumnAuditLogRetention = Column{ + name: projection.LimitsColumnAuditLogRetention, + table: limitSettingsTable, + } +) + +type Limits struct { + AggregateID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + + AuditLogRetention *time.Duration +} + +func (q *Queries) Limits(ctx context.Context, resourceOwner string) (limits *Limits, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + stmt, scan := prepareLimitsQuery(ctx, q.client) + query, args, err := stmt.Where(sq.Eq{ + LimitsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + LimitsColumnResourceOwner.identifier(): resourceOwner, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-jJe80", "Errors.Query.SQLStatment") + } + + err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { + limits, err = scan(row) + return err + }, query, args...) + return limits, err +} + +func prepareLimitsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Limits, error)) { + return sq.Select( + LimitsColumnAggregateID.identifier(), + LimitsColumnCreationDate.identifier(), + LimitsColumnChangeDate.identifier(), + LimitsColumnResourceOwner.identifier(), + LimitsColumnSequence.identifier(), + LimitsColumnAuditLogRetention.identifier(), + ). + From(limitSettingsTable.identifier() + db.Timetravel(call.Took(ctx))). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Limits, error) { + var ( + limits = new(Limits) + auditLogRetention database.NullDuration + ) + err := row.Scan( + &limits.AggregateID, + &limits.CreationDate, + &limits.ChangeDate, + &limits.ResourceOwner, + &limits.Sequence, + &auditLogRetention, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-GU1em", "Errors.Limits.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-00jgy", "Errors.Internal") + } + if auditLogRetention.Valid { + limits.AuditLogRetention = &auditLogRetention.Duration + } + return limits, nil + } +} diff --git a/internal/query/limits_test.go b/internal/query/limits_test.go new file mode 100644 index 0000000000..84e6e70e52 --- /dev/null +++ b/internal/query/limits_test.go @@ -0,0 +1,116 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + "time" + + "github.com/muhlemmer/gu" + + errs "github.com/zitadel/zitadel/internal/errors" +) + +var ( + expectedLimitsQuery = regexp.QuoteMeta("SELECT projections.limits.aggregate_id," + + " projections.limits.creation_date," + + " projections.limits.change_date," + + " projections.limits.resource_owner," + + " projections.limits.sequence," + + " projections.limits.audit_log_retention" + + " FROM projections.limits" + + " AS OF SYSTEM TIME '-1 ms'", + ) + + limitsCols = []string{ + "aggregate_id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "audit_log_retention", + } +) + +func Test_LimitsPrepare(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareLimitsQuery no result", + prepare: prepareLimitsQuery, + want: want{ + sqlExpectations: mockQueriesScanErr( + expectedLimitsQuery, + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*Limits)(nil), + }, + { + name: "prepareLimitsQuery", + prepare: prepareLimitsQuery, + want: want{ + sqlExpectations: mockQuery( + expectedLimitsQuery, + limitsCols, + []driver.Value{ + "limits1", + testNow, + testNow, + "instance1", + 0, + intervalDriverValue(t, time.Hour), + }, + ), + }, + object: &Limits{ + AggregateID: "limits1", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "instance1", + Sequence: 0, + AuditLogRetention: gu.Ptr(time.Hour), + }, + }, + { + name: "prepareLimitsQuery sql err", + prepare: prepareLimitsQuery, + want: want{ + sqlExpectations: mockQueryErr( + expectedLimitsQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*Limits)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + }) + } +} diff --git a/internal/query/prepare_test.go b/internal/query/prepare_test.go index c9770c0dd1..dadf95f000 100644 --- a/internal/query/prepare_test.go +++ b/internal/query/prepare_test.go @@ -13,13 +13,16 @@ import ( "github.com/DATA-DOG/go-sqlmock" sq "github.com/Masterminds/squirrel" + "github.com/jackc/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/database" ) var ( testNow = time.Now() + dayNow = testNow.Truncate(24 * time.Hour) ) // assertPrepare checks if the prepare func executes the correct sql query and returns the correct object @@ -385,6 +388,15 @@ func TestValidatePrepare(t *testing.T) { } } +func intervalDriverValue(t *testing.T, src time.Duration) pgtype.Interval { + interval := pgtype.Interval{} + err := interval.Set(src) + if err != nil { + t.Fatal(err) + } + return interval +} + type prepareDB struct{} const asOfSystemTime = " AS OF SYSTEM TIME '-1 ms' " diff --git a/internal/query/projection/limits.go b/internal/query/projection/limits.go new file mode 100644 index 0000000000..65487a9429 --- /dev/null +++ b/internal/query/projection/limits.go @@ -0,0 +1,114 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/limits" +) + +const ( + LimitsProjectionTable = "projections.limits" + + LimitsColumnAggregateID = "aggregate_id" + LimitsColumnCreationDate = "creation_date" + LimitsColumnChangeDate = "change_date" + LimitsColumnResourceOwner = "resource_owner" + LimitsColumnInstanceID = "instance_id" + LimitsColumnSequence = "sequence" + + LimitsColumnAuditLogRetention = "audit_log_retention" +) + +type limitsProjection struct{} + +func newLimitsProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, &limitsProjection{}) +} + +func (*limitsProjection) Name() string { + return LimitsProjectionTable +} + +func (*limitsProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(LimitsColumnAggregateID, handler.ColumnTypeText), + handler.NewColumn(LimitsColumnCreationDate, handler.ColumnTypeTimestamp), + handler.NewColumn(LimitsColumnChangeDate, handler.ColumnTypeTimestamp), + handler.NewColumn(LimitsColumnResourceOwner, handler.ColumnTypeText), + handler.NewColumn(LimitsColumnInstanceID, handler.ColumnTypeText), + handler.NewColumn(LimitsColumnSequence, handler.ColumnTypeInt64), + handler.NewColumn(LimitsColumnAuditLogRetention, handler.ColumnTypeInterval, handler.Nullable()), + }, + handler.NewPrimaryKey(LimitsColumnInstanceID, LimitsColumnResourceOwner), + ), + ) +} + +func (p *limitsProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: limits.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: limits.SetEventType, + Reduce: p.reduceLimitsSet, + }, + { + Event: limits.ResetEventType, + Reduce: p.reduceLimitsReset, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(LimitsColumnInstanceID), + }, + }, + }, + } +} + +func (p *limitsProjection) reduceLimitsSet(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*limits.SetEvent](event) + if err != nil { + return nil, err + } + conflictCols := []handler.Column{ + handler.NewCol(LimitsColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(LimitsColumnResourceOwner, e.Aggregate().ResourceOwner), + } + updateCols := []handler.Column{ + handler.NewCol(LimitsColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCol(LimitsColumnResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(LimitsColumnCreationDate, e.CreationDate()), + handler.NewCol(LimitsColumnChangeDate, e.CreationDate()), + handler.NewCol(LimitsColumnSequence, e.Sequence()), + handler.NewCol(LimitsColumnAggregateID, e.Aggregate().ID), + } + if e.AuditLogRetention != nil { + updateCols = append(updateCols, handler.NewCol(LimitsColumnAuditLogRetention, *e.AuditLogRetention)) + } + return handler.NewUpsertStatement(e, conflictCols, updateCols), nil +} + +func (p *limitsProjection) reduceLimitsReset(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*limits.ResetEvent](event) + if err != nil { + return nil, err + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(LimitsColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCond(LimitsColumnResourceOwner, e.Aggregate().ResourceOwner), + }, + ), nil +} diff --git a/internal/query/projection/limits_test.go b/internal/query/projection/limits_test.go new file mode 100644 index 0000000000..0277e29243 --- /dev/null +++ b/internal/query/projection/limits_test.go @@ -0,0 +1,96 @@ +package projection + +import ( + "testing" + "time" + + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/limits" +) + +func TestLimitsProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceLimitsSet", + args: args{ + event: getEvent(testEvent( + limits.SetEventType, + limits.AggregateType, + []byte(`{ + "auditLogRetention": 300000000000 + }`), + ), limits.SetEventMapper), + }, + reduce: (&limitsProjection{}).reduceLimitsSet, + want: wantReduce{ + aggregateType: eventstore.AggregateType("limits"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.limits (instance_id, resource_owner, creation_date, change_date, sequence, aggregate_id, audit_log_retention) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, resource_owner) DO UPDATE SET (creation_date, change_date, sequence, aggregate_id, audit_log_retention) = (EXCLUDED.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.aggregate_id, EXCLUDED.audit_log_retention)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + anyArg{}, + anyArg{}, + uint64(15), + "agg-id", + time.Minute * 5, + }, + }, + }, + }, + }, + }, + + { + name: "reduceLimitsReset", + args: args{ + event: getEvent(testEvent( + limits.ResetEventType, + limits.AggregateType, + []byte(`{}`), + ), limits.ResetEventMapper), + }, + reduce: (&limitsProjection{}).reduceLimitsReset, + want: wantReduce{ + aggregateType: eventstore.AggregateType("limits"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.limits WHERE (instance_id = $1) AND (resource_owner = $2)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if !errors.IsErrorInvalidArgument(err) { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, LimitsProjectionTable, tt.want) + }) + } +} diff --git a/internal/query/projection/main_test.go b/internal/query/projection/main_test.go index 09754b140e..460e8f07d5 100644 --- a/internal/query/projection/main_test.go +++ b/internal/query/projection/main_test.go @@ -11,6 +11,7 @@ import ( action_repo "github.com/zitadel/zitadel/internal/repository/action" iam_repo "github.com/zitadel/zitadel/internal/repository/instance" key_repo "github.com/zitadel/zitadel/internal/repository/keypair" + "github.com/zitadel/zitadel/internal/repository/limits" "github.com/zitadel/zitadel/internal/repository/org" proj_repo "github.com/zitadel/zitadel/internal/repository/project" quota_repo "github.com/zitadel/zitadel/internal/repository/quota" @@ -36,6 +37,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore { usr_repo.RegisterEventMappers(es) proj_repo.RegisterEventMappers(es) quota_repo.RegisterEventMappers(es) + limits.RegisterEventMappers(es) usergrant.RegisterEventMappers(es) key_repo.RegisterEventMappers(es) action_repo.RegisterEventMappers(es) diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 845395ec27..c95c60c6d5 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -69,6 +69,7 @@ var ( AuthRequestProjection *handler.Handler MilestoneProjection *handler.Handler QuotaProjection *quotaProjection + LimitsProjection *handler.Handler ) type projection interface { @@ -141,6 +142,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, AuthRequestProjection = newAuthRequestProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["auth_requests"])) MilestoneProjection = newMilestoneProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["milestones"]), systemUsers) QuotaProjection = newQuotaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["quotas"])) + LimitsProjection = newLimitsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["limits"])) newProjectionsList() return nil } @@ -244,5 +246,6 @@ func newProjectionsList() { AuthRequestProjection, MilestoneProjection, QuotaProjection.handler, + LimitsProjection, } } diff --git a/internal/query/query.go b/internal/query/query.go index 2265183e1a..cb50ce1fcd 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -24,6 +24,7 @@ import ( "github.com/zitadel/zitadel/internal/repository/idpintent" iam_repo "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/keypair" + "github.com/zitadel/zitadel/internal/repository/limits" "github.com/zitadel/zitadel/internal/repository/oidcsession" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" @@ -50,6 +51,7 @@ type Queries struct { supportedLangs []language.Tag zitadelRoles []authz.RoleMapping multifactors domain.MultifactorConfigs + defaultAuditLogRetention time.Duration } func StartQueries( @@ -62,6 +64,7 @@ func StartQueries( zitadelRoles []authz.RoleMapping, sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error), permissionCheck func(q *Queries) domain.PermissionCheck, + defaultAuditLogRetention time.Duration, systemAPIUsers map[string]*internal_authz.SystemAPIUser, ) (repo *Queries, err error) { statikLoginFS, err := fs.NewWithNamespace("login") @@ -84,6 +87,7 @@ func StartQueries( NotificationTranslationFileContents: make(map[string][]byte), zitadelRoles: zitadelRoles, sessionTokenVerifier: sessionTokenVerifier, + defaultAuditLogRetention: defaultAuditLogRetention, } iam_repo.RegisterEventMappers(repo.eventstore) usr_repo.RegisterEventMappers(repo.eventstore) @@ -97,6 +101,7 @@ func StartQueries( authrequest.RegisterEventMappers(repo.eventstore) oidcsession.RegisterEventMappers(repo.eventstore) quota.RegisterEventMappers(repo.eventstore) + limits.RegisterEventMappers(repo.eventstore) repo.idpConfigEncryption = idpConfigEncryption repo.multifactors = domain.MultifactorConfigs{ diff --git a/internal/query/quota_test.go b/internal/query/quota_test.go index 05bddc8031..c96af861a0 100644 --- a/internal/query/quota_test.go +++ b/internal/query/quota_test.go @@ -9,8 +9,6 @@ import ( "testing" "time" - "github.com/jackc/pgtype" - errs "github.com/zitadel/zitadel/internal/errors" ) @@ -33,19 +31,6 @@ var ( } ) -func dayNow() time.Time { - return time.Now().Truncate(24 * time.Hour) -} - -func interval(t *testing.T, src time.Duration) pgtype.Interval { - interval := pgtype.Interval{} - err := interval.Set(src) - if err != nil { - t.Fatal(err) - } - return interval -} - func Test_QuotaPrepare(t *testing.T) { type want struct { sqlExpectations sqlExpectation @@ -84,8 +69,8 @@ func Test_QuotaPrepare(t *testing.T) { quotaCols, []driver.Value{ "quota-id", - dayNow(), - interval(t, time.Hour*24), + dayNow, + intervalDriverValue(t, time.Hour*24), uint64(1000), true, testNow, @@ -94,9 +79,9 @@ func Test_QuotaPrepare(t *testing.T) { }, object: &Quota{ ID: "quota-id", - From: dayNow(), + From: dayNow, ResetInterval: time.Hour * 24, - CurrentPeriodStart: dayNow(), + CurrentPeriodStart: dayNow, Amount: 1000, Limit: true, }, diff --git a/internal/repository/limits/aggregate.go b/internal/repository/limits/aggregate.go new file mode 100644 index 0000000000..37aa38618f --- /dev/null +++ b/internal/repository/limits/aggregate.go @@ -0,0 +1,26 @@ +package limits + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + AggregateType = "limits" + AggregateVersion = "v1" +) + +type Aggregate struct { + eventstore.Aggregate +} + +func NewAggregate(id, instanceId, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Type: AggregateType, + Version: AggregateVersion, + ID: id, + InstanceID: instanceId, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/limits/events.go b/internal/repository/limits/events.go new file mode 100644 index 0000000000..47bde7cffc --- /dev/null +++ b/internal/repository/limits/events.go @@ -0,0 +1,86 @@ +package limits + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + eventTypePrefix = eventstore.EventType("limits.") + SetEventType = eventTypePrefix + "set" + ResetEventType = eventTypePrefix + "reset" +) + +// SetEvent describes that limits are added or modified and contains only changed properties +type SetEvent struct { + *eventstore.BaseEvent `json:"-"` + AuditLogRetention *time.Duration `json:"auditLogRetention,omitempty"` +} + +func (e *SetEvent) Payload() any { + return e +} + +func (e *SetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *SetEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func NewSetEvent( + base *eventstore.BaseEvent, + changes ...LimitsChange, +) *SetEvent { + changedEvent := &SetEvent{ + BaseEvent: base, + } + for _, change := range changes { + change(changedEvent) + } + return changedEvent +} + +type LimitsChange func(*SetEvent) + +func ChangeAuditLogRetention(auditLogRetention *time.Duration) LimitsChange { + return func(e *SetEvent) { + e.AuditLogRetention = auditLogRetention + } +} + +var SetEventMapper = eventstore.GenericEventMapper[SetEvent] + +type ResetEvent struct { + *eventstore.BaseEvent `json:"-"` +} + +func (e *ResetEvent) Payload() any { + return e +} + +func (e *ResetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *ResetEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func NewResetEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *ResetEvent { + return &ResetEvent{ + BaseEvent: eventstore.NewBaseEventForPush( + ctx, + aggregate, + ResetEventType, + ), + } +} + +var ResetEventMapper = eventstore.GenericEventMapper[ResetEvent] diff --git a/internal/repository/limits/eventstore.go b/internal/repository/limits/eventstore.go new file mode 100644 index 0000000000..5c2ab16c4f --- /dev/null +++ b/internal/repository/limits/eventstore.go @@ -0,0 +1,10 @@ +package limits + +import ( + "github.com/zitadel/zitadel/internal/eventstore" +) + +func RegisterEventMappers(es *eventstore.Eventstore) { + es.RegisterFilterEventMapper(AggregateType, SetEventType, SetEventMapper). + RegisterFilterEventMapper(AggregateType, ResetEventType, ResetEventMapper) +} diff --git a/internal/repository/quota/events.go b/internal/repository/quota/events.go index b52226689a..616f61faed 100644 --- a/internal/repository/quota/events.go +++ b/internal/repository/quota/events.go @@ -27,14 +27,6 @@ const ( ActionsAllRunsSeconds ) -func NewAddQuotaUnitUniqueConstraint(unit Unit) *eventstore.UniqueConstraint { - return eventstore.NewAddEventUniqueConstraint( - UniqueQuotaNameType, - strconv.FormatUint(uint64(unit), 10), - "Errors.Quota.AlreadyExists", - ) -} - func NewRemoveQuotaNameUniqueConstraint(unit Unit) *eventstore.UniqueConstraint { return eventstore.NewRemoveUniqueConstraint( UniqueQuotaNameType, diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index b516fcadb6..9c5784e3bd 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: Обектът не можа да бъде премахнат Limit: ExceedsDefault: Лимитът надвишава лимита по подразбиране + Limits: + NotFound: Лимитът не е намерен + NoneSpecified: Не са посочени лимити Language: NotParsed: Езикът не можа да бъде анализиран синтактично OIDCSettings: diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 7610d69401..3fe394ce08 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: Objekt konnte nicht gelöscht werden Limit: ExceedsDefault: Limit überschreitet default Limit + Limits: + NotFound: Limits konnten nicht gefunden werden + NoneSpecified: Keine Limits angegeben Language: NotParsed: Sprache konnte nicht gemapped werden OIDCSettings: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 0c694f463e..17a7dea05e 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: Object could not be removed Limit: ExceedsDefault: Limit exceeds default limit + Limits: + NotFound: Limits not found + NoneSpecified: No limits specified Language: NotParsed: Could not parse language OIDCSettings: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 9776a61f23..e4e62c72af 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: El objeto no pudo eliminarse Limit: ExceedsDefault: El límite excede el límite por defecto + Limits: + NotFound: Límite no encontrado + NoneSpecified: No se especificaron límites Language: NotParsed: No pude analizar el idioma OIDCSettings: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index b72a5ba1f3..4defb4e449 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: L'objet n'a pas pu être retiré Limit: ExceedsDefault: La limite dépasse la limite par défaut + Limits: + NotFound: Limites non trouvée + NoneSpecified: Aucune limite spécifiée Language: NotParsed: Impossible d'analyser la langue OIDCSettings: diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 43891039ea..b8be789620 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: L'oggetto non può essere rimosso Limit: ExceedsDefault: Il limite supera quello predefinito + Limits: + NotFound: Limite non trovato + NoneSpecified: Nessun limite specificato Language: NotParsed: Impossibile analizzare la lingua OIDCSettings: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 72a7aa9d32..d615c1c9b5 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: オブジェクトの削除に失敗しました Limit: ExceedsDefault: デフォルトの制限を超えています + Limits: + NotFound: 制限が見つかりません + NoneSpecified: 制限が指定されていません Language: NotParsed: 言語のパースに失敗しました OIDCSettings: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index ae27d818b2..e6ee3052d7 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: Објектот не може да се отстрани Limit: ExceedsDefault: Лимитот го надминува стандардниот лимит + Limits: + NotFound: Лимитот не е пронајден + NoneSpecified: Не се наведени лимити Language: NotParsed: Јазикот не може да се парсира OIDCSettings: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 9e7a55ef0f..600c3feac4 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: Obiekt nie mógł zostać usunięty Limit: ExceedsDefault: Limit przekracza domyślny limit + Limits: + NotFound: Limit nie znaleziony + NoneSpecified: Nie określono limitów Language: NotParsed: Nie można przeanalizować języka OIDCSettings: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 71448e1a89..ae911dcc6a 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: Não foi possível remover o objeto Limit: ExceedsDefault: Limite excede o limite padrão + Limits: + NotFound: Limite não encontrado + NoneSpecified: Nenhum limite especificado Language: NotParsed: Não foi possível analisar o idioma OIDCSettings: diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 526e67f055..28631eba4e 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -28,6 +28,9 @@ Errors: RemoveFailed: 无法移除对象 Limit: ExceedsDefault: 超出默认限制 + Limits: + NotFound: 未找到限制 + NoneSpecified: 未指定限制 Language: NotParsed: 无法解析语言 OIDCSettings: diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index ed2e73d29d..80fc89076d 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -365,6 +365,10 @@ service SystemService { // Returns an error if the quota already exists for the specified unit // Deprecated: use SetQuota instead rpc AddQuota(AddQuotaRequest) returns (AddQuotaResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: ["Usage Control", "Quotas"]; + }; + option (google.api.http) = { post: "/instances/{instance_id}/quotas" body: "*" @@ -378,6 +382,10 @@ service SystemService { // Sets quota configuration properties // Creates a new quota if it doesn't exist for the specified unit rpc SetQuota(SetQuotaRequest) returns (SetQuotaResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: ["Usage Control", "Quotas"]; + }; + option (google.api.http) = { put: "/instances/{instance_id}/quotas" body: "*" @@ -390,6 +398,10 @@ service SystemService { // Removes a quota rpc RemoveQuota(RemoveQuotaRequest) returns (RemoveQuotaResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: ["Usage Control", "Quotas"]; + }; + option (google.api.http) = { delete: "/instances/{instance_id}/quotas/{unit}" }; @@ -410,6 +422,71 @@ service SystemService { permission: "authenticated"; }; } + + // Sets instance level limits + rpc SetLimits(SetLimitsRequest) returns (SetLimitsResponse) { + option (google.api.http) = { + put: "/instances/{instance_id}/limits" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: ["Usage Control", "Limits"]; + responses: { + key: "200"; + value: { + description: "Instance limits set"; + }; + }; + responses: { + key: "400"; + value: { + description: "At least one limit must be specified"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // Resets instance level limits + rpc ResetLimits(ResetLimitsRequest) returns (ResetLimitsResponse) { + option (google.api.http) = { + delete: "/instances/{instance_id}/limits" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: ["Usage Control", "Limits"]; + responses: { + key: "200"; + value: { + description: "Limits are reset to the system defaults"; + }; + }; + responses: { + key: "404"; + value: { + description: "Limits are already set to the system defaults"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } } @@ -683,6 +760,27 @@ message RemoveQuotaResponse { zitadel.v1.ObjectDetails details = 1; } +message SetLimitsRequest { + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + google.protobuf.Duration audit_log_retention = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "AuditLogRetention limits the number of events that can be queried via the events API by their age. A value of '0s' means that all events are available. If this value is set, it overwrites the system default."; + } + ]; +} + +message SetLimitsResponse { + zitadel.v1.ObjectDetails details = 1; +} + +message ResetLimitsRequest { + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ResetLimitsResponse { + zitadel.v1.ObjectDetails details = 1; +} + message ExistsDomainRequest { string domain = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -906,4 +1004,4 @@ message SetInstanceFeatureRequest { message SetInstanceFeatureResponse { zitadel.v1.ObjectDetails details = 1; -} \ No newline at end of file +} From 48ae5d58ac7507bb350e7e0d01dc6e6dec870995 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:09:15 +0200 Subject: [PATCH 29/48] =?UTF-8?q?feat:=20add=20activity=20logs=20on=20user?= =?UTF-8?q?=20actions=20with=20authentication,=20resource=E2=80=A6=20(#674?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add activity logs on user actions with authentication, resourceAPI and sessionAPI * feat: add activity logs on user actions with authentication, resourceAPI and sessionAPI * feat: add activity logs on user actions with authentication, resourceAPI and sessionAPI * feat: add activity logs on user actions with authentication, resourceAPI and sessionAPI * feat: add activity logs on user actions with authentication, resourceAPI and sessionAPI * fix: add unit tests to info package for context changes * fix: add activity_interceptor.go suggestion Co-authored-by: Tim Möhlmann * fix: refactoring and fixes through PR review * fix: add auth service to lists of resourceAPIs --------- Co-authored-by: Tim Möhlmann Co-authored-by: Fabi --- cmd/start/start.go | 5 +- internal/activity/activity.go | 73 +++++++++ internal/api/grpc/admin/server.go | 4 + internal/api/grpc/management/server.go | 4 + .../server/middleware/activity_interceptor.go | 35 ++++ internal/api/grpc/server/server.go | 1 + internal/api/grpc/session/v2/session.go | 6 + .../http/middleware/activity_interceptor.go | 32 ++++ internal/api/info/info.go | 43 +++++ internal/api/info/info_test.go | 117 +++++++++++++ internal/api/oidc/auth_request.go | 18 ++ internal/api/saml/storage.go | 4 + .../handlers/quota_notifier_test.go | 155 ++++++++++++++++++ 13 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 internal/activity/activity.go create mode 100644 internal/api/grpc/server/middleware/activity_interceptor.go create mode 100644 internal/api/http/middleware/activity_interceptor.go create mode 100644 internal/api/info/info.go create mode 100644 internal/api/info/info_test.go create mode 100644 internal/notification/handlers/quota_notifier_test.go diff --git a/cmd/start/start.go b/cmd/start/start.go index 078075a17b..9da5c114ff 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -316,8 +316,11 @@ func startAPIs( authZRepo, queries, } + oidcPrefixes := []string{"/.well-known/openid-configuration", "/oidc/v1", "/oauth/v2"} // always set the origin in the context if available in the http headers, no matter for what protocol router.Use(middleware.OriginHandler) + // adds used HTTPPathPattern and RequestMethod to context + router.Use(middleware.ActivityHandler(append(oidcPrefixes, saml.HandlerPrefix, admin.GatewayPathPrefix(), management.GatewayPathPrefix()))) verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) tlsConfig, err := config.TLS.Config() if err != nil { @@ -413,7 +416,7 @@ func startAPIs( 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") + apis.RegisterHandlerPrefixes(oidcProvider.HttpHandler(), oidcPrefixes...) samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, limitingAccessInterceptor) if err != nil { diff --git a/internal/activity/activity.go b/internal/activity/activity.go new file mode 100644 index 0000000000..4ceb681cf7 --- /dev/null +++ b/internal/activity/activity.go @@ -0,0 +1,73 @@ +package activity + +import ( + "context" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + http_utils "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/api/info" +) + +const ( + Activity = "activity" +) + +type TriggerMethod int + +const ( + Unspecified TriggerMethod = iota + ResourceAPI + OIDCAccessToken + OIDCRefreshToken + SessionAPI + SAMLResponse +) + +func (t TriggerMethod) String() string { + switch t { + case Unspecified: + return "unspecified" + case ResourceAPI: + return "resourceAPI" + case OIDCRefreshToken: + return "refreshToken" + case OIDCAccessToken: + return "accessToken" + case SessionAPI: + return "sessionAPI" + case SAMLResponse: + return "samlResponse" + default: + return "unknown" + } +} + +func Trigger(ctx context.Context, orgID, userID string, trigger TriggerMethod) { + triggerLog(authz.GetInstance(ctx).InstanceID(), orgID, userID, http_utils.ComposedOrigin(ctx), trigger, info.ActivityInfoFromContext(ctx)) +} + +func TriggerWithContext(ctx context.Context, trigger TriggerMethod) { + data := authz.GetCtxData(ctx) + ai := info.ActivityInfoFromContext(ctx) + // if GRPC call, path is prefilled with the grpc fullmethod and method is empty + if ai.Method == "" { + ai.Method = ai.Path + ai.Path = "" + } + triggerLog(authz.GetInstance(ctx).InstanceID(), data.OrgID, data.UserID, http_utils.ComposedOrigin(ctx), trigger, ai) +} + +func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, ai *info.ActivityInfo) { + logging.WithFields( + "instance", instanceID, + "org", orgID, + "user", userID, + "domain", domain, + "trigger", trigger.String(), + "method", ai.Method, + "path", ai.Path, + "requestMethod", ai.RequestMethod, + ).Info(Activity) +} diff --git a/internal/api/grpc/admin/server.go b/internal/api/grpc/admin/server.go index 7fe8e32f34..e367765983 100644 --- a/internal/api/grpc/admin/server.go +++ b/internal/api/grpc/admin/server.go @@ -79,5 +79,9 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc { } func (s *Server) GatewayPathPrefix() string { + return GatewayPathPrefix() +} + +func GatewayPathPrefix() string { return "/admin/v1" } diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index b0f9879278..638da236b7 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -71,5 +71,9 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc { } func (s *Server) GatewayPathPrefix() string { + return GatewayPathPrefix() +} + +func GatewayPathPrefix() string { return "/management/v1" } diff --git a/internal/api/grpc/server/middleware/activity_interceptor.go b/internal/api/grpc/server/middleware/activity_interceptor.go new file mode 100644 index 0000000000..7b6f050265 --- /dev/null +++ b/internal/api/grpc/server/middleware/activity_interceptor.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "context" + "slices" + "strings" + + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/activity" +) + +func ActivityInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + resp, err := handler(ctx, req) + if isResourceAPI(info.FullMethod) { + activity.TriggerWithContext(ctx, activity.ResourceAPI) + } + return resp, err + } +} + +var resourcePrefixes = []string{ + "/zitadel.management.v1.ManagementService/", + "/zitadel.admin.v1.AdminService/", + "/zitadel.user.v2beta.UserService/", + "/zitadel.settings.v2beta.SettingsService/", + "/zitadel.auth.v1.AuthService/", +} + +func isResourceAPI(method string) bool { + return slices.ContainsFunc(resourcePrefixes, func(prefix string) bool { + return strings.HasPrefix(method, prefix) + }) +} diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 9d7deb28ea..96c8066d1e 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -58,6 +58,7 @@ func CreateServer( middleware.TranslationHandler(), middleware.ValidationHandler(), middleware.ServiceHandler(), + middleware.ActivityInterceptor(), ), ), } diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index f98983936d..c766b3ef18 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -9,6 +9,8 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/activity" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/command" @@ -57,6 +59,7 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe if err != nil { return nil, err } + return &session.CreateSessionResponse{ Details: object.DomainToDetailsPb(set.ObjectDetails), SessionId: set.ID, @@ -310,6 +313,9 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([ if err != nil { return nil, err } + + // trigger activity log for session for user + activity.Trigger(ctx, user.ResourceOwner, user.ID, activity.SessionAPI) sessionChecks = append(sessionChecks, command.CheckUser(user.ID)) } if password := checks.GetPassword(); password != nil { diff --git a/internal/api/http/middleware/activity_interceptor.go b/internal/api/http/middleware/activity_interceptor.go new file mode 100644 index 0000000000..7cba3db421 --- /dev/null +++ b/internal/api/http/middleware/activity_interceptor.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/zitadel/zitadel/internal/api/info" +) + +func ActivityHandler(handlerPrefixes []string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + activityInfo := info.ActivityInfoFromContext(ctx) + hasPrefix := false + // only add path to context if handler is called + for _, prefix := range handlerPrefixes { + if strings.HasPrefix(r.URL.Path, prefix) { + activityInfo.SetPath(r.URL.Path) + hasPrefix = true + break + } + } + // last call is with grpc method as path + if !hasPrefix { + activityInfo.SetMethod(r.URL.Path) + } + ctx = activityInfo.SetRequestMethod(r.Method).IntoContext(ctx) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/internal/api/info/info.go b/internal/api/info/info.go new file mode 100644 index 0000000000..53d53518b1 --- /dev/null +++ b/internal/api/info/info.go @@ -0,0 +1,43 @@ +package info + +import ( + "context" +) + +type activityInfoKey struct{} + +type ActivityInfo struct { + Method string + Path string + RequestMethod string +} + +func (a *ActivityInfo) IntoContext(ctx context.Context) context.Context { + return context.WithValue(ctx, activityInfoKey{}, a) +} + +func ActivityInfoFromContext(ctx context.Context) *ActivityInfo { + m := ctx.Value(activityInfoKey{}) + if m == nil { + return &ActivityInfo{} + } + ai, ok := m.(*ActivityInfo) + if !ok { + return &ActivityInfo{} + } + return ai +} + +func (a *ActivityInfo) SetMethod(method string) *ActivityInfo { + a.Method = method + return a +} +func (a *ActivityInfo) SetPath(path string) *ActivityInfo { + a.Path = path + return a +} + +func (a *ActivityInfo) SetRequestMethod(method string) *ActivityInfo { + a.RequestMethod = method + return a +} diff --git a/internal/api/info/info_test.go b/internal/api/info/info_test.go new file mode 100644 index 0000000000..0b9ecc5fd6 --- /dev/null +++ b/internal/api/info/info_test.go @@ -0,0 +1,117 @@ +package info + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ActivityInfo(t *testing.T) { + type args struct { + ctx context.Context + ok bool + path string + method string + requestMethod string + } + type want struct { + ok bool + path string + method string + requestMethod string + } + tests := []struct { + name string + args args + want want + }{ + { + "already set", + args{ + ctx: ctxWithActivityInfo(context.Background(), "set", "set", "set"), + ok: false, + }, + want{ + ok: true, + path: "set", + method: "set", + requestMethod: "set", + }, + }, + { + "not set, empty", + args{ + ctx: context.Background(), + ok: false, + }, + want{ + ok: true, + }, + }, + { + "set empty", + args{ + ctx: context.Background(), + ok: true, + }, + want{ + ok: true, + }, + }, + { + "set", + args{ + ctx: context.Background(), + ok: true, + path: "set", + method: "set", + requestMethod: "set", + }, + want{ + ok: true, + path: "set", + method: "set", + requestMethod: "set", + }, + }, + { + "reset", + args{ + ctx: ctxWithActivityInfo(context.Background(), "set", "set", "set"), + ok: true, + path: "set2", + method: "set2", + requestMethod: "set2", + }, + want{ + ok: true, + path: "set2", + method: "set2", + requestMethod: "set2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ai := &ActivityInfo{} + ai.SetMethod(tt.args.method).SetPath(tt.args.path).SetRequestMethod(tt.args.requestMethod) + if tt.args.ok { + tt.args.ctx = ai.IntoContext(tt.args.ctx) + } + + res := ActivityInfoFromContext(tt.args.ctx) + if tt.want.ok { + assert.NotNil(t, res) + } + assert.Equal(t, tt.want.path, res.Path) + assert.Equal(t, tt.want.method, res.Method) + assert.Equal(t, tt.want.requestMethod, res.RequestMethod) + }) + } +} + +func ctxWithActivityInfo(ctx context.Context, method, path, requestMethod string) context.Context { + ai := &ActivityInfo{} + return ai.SetPath(path).SetRequestMethod(requestMethod).SetMethod(method).IntoContext(ctx) +} diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 56c0902b11..267f674b2f 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -10,6 +10,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/zitadel/internal/activity" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -196,6 +197,8 @@ func (o *OPStorage) CreateAccessToken(ctx context.Context, req op.TokenRequest) applicationID = authReq.ApplicationID userOrgID = authReq.UserOrgID case *AuthRequestV2: + // trigger activity log for authentication for user + activity.Trigger(ctx, "", authReq.CurrentAuthRequest.UserID, activity.OIDCAccessToken) return o.command.AddOIDCSessionAccessToken(setContextUserSystem(ctx), authReq.GetID()) } @@ -208,6 +211,9 @@ func (o *OPStorage) CreateAccessToken(ctx context.Context, req op.TokenRequest) if err != nil { return "", time.Time{}, err } + + // trigger activity log for authentication for user + activity.Trigger(ctx, userOrgID, req.GetSubject(), activity.OIDCAccessToken) return resp.TokenID, resp.Expiration, nil } @@ -218,8 +224,12 @@ func (o *OPStorage) CreateAccessAndRefreshTokens(ctx context.Context, req op.Tok // handle V2 request directly switch tokenReq := req.(type) { case *AuthRequestV2: + // trigger activity log for authentication for user + activity.Trigger(ctx, "", tokenReq.GetSubject(), activity.OIDCRefreshToken) return o.command.AddOIDCSessionRefreshAndAccessToken(setContextUserSystem(ctx), tokenReq.GetID()) case *RefreshTokenRequestV2: + // trigger activity log for authentication for user + activity.Trigger(ctx, "", tokenReq.GetSubject(), activity.OIDCRefreshToken) return o.command.ExchangeOIDCSessionRefreshAndAccessToken(setContextUserSystem(ctx), tokenReq.OIDCSessionWriteModel.AggregateID, refreshToken, tokenReq.RequestedScopes) } @@ -246,6 +256,9 @@ func (o *OPStorage) CreateAccessAndRefreshTokens(ctx context.Context, req op.Tok } return "", "", time.Time{}, err } + + // trigger activity log for authentication for user + activity.Trigger(ctx, userOrgID, req.GetSubject(), activity.OIDCRefreshToken) return resp.TokenID, token, resp.Expiration, nil } @@ -274,6 +287,8 @@ func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken if err != nil { return nil, err } + // trigger activity log for authentication for user + activity.Trigger(ctx, "", oidcSession.UserID, activity.OIDCRefreshToken) return &RefreshTokenRequestV2{OIDCSessionWriteModel: oidcSession}, nil } @@ -281,6 +296,9 @@ func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken if err != nil { return nil, err } + + // trigger activity log for use of refresh token for user + activity.Trigger(ctx, tokenView.ResourceOwner, tokenView.UserID, activity.OIDCRefreshToken) return RefreshTokenRequestFromBusiness(tokenView), nil } diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index c5d348ccb7..87e6960d48 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -15,6 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/actions/object" + "github.com/zitadel/zitadel/internal/activity" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/auth/repository" "github.com/zitadel/zitadel/internal/command" @@ -148,6 +149,9 @@ func (p *Storage) SetUserinfoWithUserID(ctx context.Context, applicationID strin } setUserinfo(user, userinfo, attributes, customAttributes) + + // trigger activity log for authentication for user + activity.Trigger(ctx, user.ResourceOwner, user.ID, activity.SAMLResponse) return nil } diff --git a/internal/notification/handlers/quota_notifier_test.go b/internal/notification/handlers/quota_notifier_test.go new file mode 100644 index 0000000000..059d4cf041 --- /dev/null +++ b/internal/notification/handlers/quota_notifier_test.go @@ -0,0 +1,155 @@ +//go:build integration + +package handlers_test + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/repository/quota" + "github.com/zitadel/zitadel/pkg/grpc/admin" + quota_pb "github.com/zitadel/zitadel/pkg/grpc/quota" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func TestServer_QuotaNotification_Limit(t *testing.T) { + _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(CTX, SystemCTX) + amount := 10 + percent := 50 + percentAmount := amount * percent / 100 + + _, err := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{ + InstanceId: instanceID, + Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED, + From: timestamppb.Now(), + ResetInterval: durationpb.New(time.Minute * 5), + Amount: uint64(amount), + Limit: true, + Notifications: []*quota_pb.Notification{ + { + Percent: uint32(percent), + Repeat: true, + CallUrl: "http://localhost:8082", + }, + { + Percent: 100, + Repeat: true, + CallUrl: "http://localhost:8082", + }, + }, + }) + require.NoError(t, err) + + for i := 0; i < percentAmount; i++ { + _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) + if err != nil { + require.NoError(t, fmt.Errorf("error in %d call of %d: %f", i, percentAmount, err)) + } + } + awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, percent) + + for i := 0; i < (amount - percentAmount); i++ { + _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) + require.NoError(t, err) + } + awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, 100) + + _, limitErr := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) + require.Error(t, limitErr) +} + +func TestServer_QuotaNotification_NoLimit(t *testing.T) { + _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(CTX, SystemCTX) + amount := 10 + percent := 50 + percentAmount := amount * percent / 100 + + _, err := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{ + InstanceId: instanceID, + Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED, + From: timestamppb.Now(), + ResetInterval: durationpb.New(time.Minute * 5), + Amount: uint64(amount), + Limit: false, + Notifications: []*quota_pb.Notification{ + { + Percent: uint32(percent), + Repeat: false, + CallUrl: "http://localhost:8082", + }, + { + Percent: 100, + Repeat: true, + CallUrl: "http://localhost:8082", + }, + }, + }) + require.NoError(t, err) + + for i := 0; i < percentAmount; i++ { + _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) + if err != nil { + require.NoError(t, fmt.Errorf("error in %d call of %d: %f", i, percentAmount, err)) + } + } + awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, percent) + + for i := 0; i < (amount - percentAmount); i++ { + _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) + if err != nil { + require.NoError(t, fmt.Errorf("error in %d call of %d: %f", percentAmount+i, amount, err)) + } + } + awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, 100) + + for i := 0; i < amount; i++ { + _, err := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) + if err != nil { + require.NoError(t, fmt.Errorf("error in %d call of %d over limit: %f", i, amount, err)) + } + } + awaitNotification(t, time.Now(), Tester.QuotaNotificationChan, quota.RequestsAllAuthenticated, 200) + + _, limitErr := Tester.Client.Admin.GetDefaultOrg(iamOwnerCtx, &admin.GetDefaultOrgRequest{}) + require.NoError(t, limitErr) +} + +func awaitNotification(t *testing.T, start time.Time, bodies chan []byte, unit quota.Unit, percent int) { + for { + select { + case body := <-bodies: + plain := new(bytes.Buffer) + if err := json.Indent(plain, body, "", " "); err != nil { + t.Fatal(err) + } + t.Log("received notificationDueEvent", plain.String()) + event := struct { + Unit quota.Unit `json:"unit"` + ID string `json:"id"` + CallURL string `json:"callURL"` + PeriodStart time.Time `json:"periodStart"` + Threshold uint16 `json:"threshold"` + Usage uint64 `json:"usage"` + }{} + if err := json.Unmarshal(body, &event); err != nil { + t.Error(err) + } + if event.ID == "" { + continue + } + if event.Unit == unit && event.Threshold == uint16(percent) { + return + } + case <-time.After(20 * time.Second): + t.Fatalf("start %s stop %s timed out waiting for unit %s and percent %d", start.Format(time.RFC3339), time.Now().Format(time.RFC3339), strconv.Itoa(int(unit)), percent) + } + } +} From c8b9b0ac75c5d6a17267f769d741a7ddbe49bd15 Mon Sep 17 00:00:00 2001 From: Silvan Date: Wed, 25 Oct 2023 16:20:55 +0200 Subject: [PATCH 30/48] docs: replace fix cockroachdb version with latest stable (#6803) --- README.md | 2 +- deploy/knative/cockroachdb-statefulset-single-node.yaml | 2 +- docs/docs/self-hosting/deploy/docker-compose-sa.yaml | 2 +- docs/docs/self-hosting/deploy/docker-compose.yaml | 2 +- docs/docs/self-hosting/deploy/linux.mdx | 2 +- .../deploy/loadbalancing-example/docker-compose.yaml | 4 ++-- docs/docs/self-hosting/deploy/macos.mdx | 2 +- docs/docs/self-hosting/manage/configure/docker-compose.yaml | 4 ++-- e2e/config/localhost/docker-compose.yaml | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 91d9ec34e4..f13f41af1d 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ Self-Service - [Administration UI (Console)](https://zitadel.com/docs/guides/manage/console/overview) Deployment -- [Postgres](https://zitadel.com/docs/self-hosting/manage/database#postgres) (version >= 14) or [CockroachDB](https://zitadel.com/docs/self-hosting/manage/database#cockroach) (version >= 22.0) +- [Postgres](https://zitadel.com/docs/self-hosting/manage/database#postgres) (version >= 14) or [CockroachDB](https://zitadel.com/docs/self-hosting/manage/database#cockroach) (version latest stable) - [Zero Downtime Updates](https://zitadel.com/docs/concepts/architecture/solution#zero-downtime-updates) Track upcoming features on our [roadmap](https://zitadel.com/roadmap). diff --git a/deploy/knative/cockroachdb-statefulset-single-node.yaml b/deploy/knative/cockroachdb-statefulset-single-node.yaml index 670dda53d8..c18b1df5d3 100644 --- a/deploy/knative/cockroachdb-statefulset-single-node.yaml +++ b/deploy/knative/cockroachdb-statefulset-single-node.yaml @@ -98,7 +98,7 @@ spec: topologyKey: kubernetes.io/hostname containers: - name: cockroachdb - image: cockroachdb/cockroach:v22.2.2 + image: cockroachdb/cockroach:latest imagePullPolicy: IfNotPresent # TODO: Change these to appropriate values for the hardware that you're running. You can see # the resources that can be allocated on each of your Kubernetes nodes by running: diff --git a/docs/docs/self-hosting/deploy/docker-compose-sa.yaml b/docs/docs/self-hosting/deploy/docker-compose-sa.yaml index 38f9b25122..34dc55c6a5 100644 --- a/docs/docs/self-hosting/deploy/docker-compose-sa.yaml +++ b/docs/docs/self-hosting/deploy/docker-compose-sa.yaml @@ -26,7 +26,7 @@ services: restart: 'always' networks: - 'zitadel' - image: 'cockroachdb/cockroach:v22.2.2' + image: 'cockroachdb/cockroach:latest' command: 'start-single-node --insecure' healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"] diff --git a/docs/docs/self-hosting/deploy/docker-compose.yaml b/docs/docs/self-hosting/deploy/docker-compose.yaml index b7c11d8dce..cfc6244922 100644 --- a/docs/docs/self-hosting/deploy/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/docker-compose.yaml @@ -20,7 +20,7 @@ services: restart: 'always' networks: - 'zitadel' - image: 'cockroachdb/cockroach:v22.2.2' + image: 'cockroachdb/cockroach:latest' command: 'start-single-node --insecure' healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"] diff --git a/docs/docs/self-hosting/deploy/linux.mdx b/docs/docs/self-hosting/deploy/linux.mdx index eab3cb50b2..d697f40a04 100644 --- a/docs/docs/self-hosting/deploy/linux.mdx +++ b/docs/docs/self-hosting/deploy/linux.mdx @@ -12,7 +12,7 @@ import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx ## Install CockroachDB Download a `cockroach` binary as described [in the CockroachDB docs](https://www.cockroachlabs.com/docs/stable/install-cockroachdb). -ZITADEL is tested against CockroachDB v22.2.2 and Ubuntu 20.04. +ZITADEL is tested against CockroachDB latest stable tag and Ubuntu 20.04. ## Run CockroachDB diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml index 21a365354a..8b5927ea67 100644 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml @@ -28,7 +28,7 @@ services: - 'zitadel-certs:/crdb-certs:ro' certs: - image: 'cockroachdb/cockroach:v22.2.2' + image: 'cockroachdb/cockroach:latest' entrypoint: [ '/bin/bash', '-c' ] command: [ 'cp /certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown 1000:1000 /zitadel-certs/*' ] volumes: @@ -42,7 +42,7 @@ services: restart: 'always' networks: - 'zitadel' - image: 'cockroachdb/cockroach:v22.2.2' + image: 'cockroachdb/cockroach:latest' command: 'start-single-node --advertise-addr my-cockroach-db' healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"] diff --git a/docs/docs/self-hosting/deploy/macos.mdx b/docs/docs/self-hosting/deploy/macos.mdx index 7b7e9e98dc..647f222136 100644 --- a/docs/docs/self-hosting/deploy/macos.mdx +++ b/docs/docs/self-hosting/deploy/macos.mdx @@ -11,7 +11,7 @@ import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx ## Install CockroachDB Download a `cockroach` binary as described [in the CockroachDB docs](https://www.cockroachlabs.com/docs/stable/install-cockroachdb). -ZITADEL is tested against CockroachDB v22.2.2. +ZITADEL is tested against CockroachDB latest stable tag. ## Run CockroachDB diff --git a/docs/docs/self-hosting/manage/configure/docker-compose.yaml b/docs/docs/self-hosting/manage/configure/docker-compose.yaml index ba03bde6c2..c31c13c5ab 100644 --- a/docs/docs/self-hosting/manage/configure/docker-compose.yaml +++ b/docs/docs/self-hosting/manage/configure/docker-compose.yaml @@ -19,7 +19,7 @@ services: - "zitadel-certs:/crdb-certs:ro" certs: - image: "cockroachdb/cockroach:v22.2.2" + image: "cockroachdb/cockroach:latest" entrypoint: ["/bin/bash", "-c"] command: [ @@ -36,7 +36,7 @@ services: restart: "always" networks: - "zitadel" - image: "cockroachdb/cockroach:v22.2.2" + image: "cockroachdb/cockroach:latest" command: "start-single-node --advertise-addr my-cockroach-db" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"] diff --git a/e2e/config/localhost/docker-compose.yaml b/e2e/config/localhost/docker-compose.yaml index 5dc5578a67..79114754ce 100644 --- a/e2e/config/localhost/docker-compose.yaml +++ b/e2e/config/localhost/docker-compose.yaml @@ -32,7 +32,7 @@ services: db: restart: 'always' - image: 'cockroachdb/cockroach:v22.2.10' + image: 'cockroachdb/cockroach:latest' command: 'start-single-node --insecure --http-addr :9090' healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:9090/health?ready=1'] From 4980cd6a0c9a1a58f3657326a28ce667350c196f Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 25 Oct 2023 17:10:45 +0200 Subject: [PATCH 31/48] feat: add SYSTEM_OWNER role (#6765) * define roles and permissions * support system user memberships * don't limit system users * cleanup permissions * restrict memberships to aggregates * default to SYSTEM_OWNER * update unit tests * test: system user token test (#6778) * update unit tests * refactor: make authz testable * move session constants * cleanup * comment * comment * decode member type string to enum (#6780) * decode member type string to enum * handle all membership types * decode enums where necessary * decode member type in steps config * update system api docs * add technical advisory * tweak docs a bit * comment in comment * lint * extract token from Bearer header prefix * review changes * fix tests * fix: add fix for activityhandler * add isSystemUser * remove IsSystemUser from activity info * fix: add fix for activityhandler --------- Co-authored-by: Stefan Benz --- cmd/defaults.yaml | 45 +++- cmd/ready/config.go | 5 +- cmd/setup/config.go | 6 +- cmd/start/config.go | 4 +- cmd/start/config_test.go | 84 ++++++++ cmd/start/start.go | 9 +- .../integrate/access-zitadel-system-api.md | 29 ++- docs/docs/support/advisory/a10007.md | 63 ++++++ docs/docs/support/technical_advisory.mdx | 12 ++ internal/activity/activity.go | 42 ++-- internal/api/api.go | 4 +- internal/api/assets/asset.go | 2 +- internal/api/authz/access_token.go | 44 ++++ .../{token_test.go => access_token_test.go} | 21 +- internal/api/authz/api_token_verifier.go | 69 +++++++ internal/api/authz/authorization.go | 4 +- internal/api/authz/context.go | 93 ++++++--- internal/api/authz/membertype_enumer.go | 94 +++++++++ internal/api/authz/permissions.go | 5 + internal/api/authz/permissions_test.go | 89 ++++---- internal/api/authz/session_token.go | 32 +++ internal/api/authz/system_token.go | 117 +++++++++++ internal/api/authz/token.go | 188 ----------------- internal/api/grpc/auth/server.go | 4 + .../server/middleware/auth_interceptor.go | 4 +- .../middleware/auth_interceptor_test.go | 195 ++++++++++++++---- .../server/middleware/quota_interceptor.go | 4 +- internal/api/grpc/server/server.go | 2 +- .../http/middleware/activity_interceptor.go | 28 +-- .../api/http/middleware/auth_interceptor.go | 6 +- .../eventstore/user_membership.go | 4 +- internal/config/hook/{feature.go => enum.go} | 11 +- proto/zitadel/admin.proto | 4 + proto/zitadel/system.proto | 46 +++-- 34 files changed, 959 insertions(+), 410 deletions(-) create mode 100644 cmd/start/config_test.go create mode 100644 docs/docs/support/advisory/a10007.md create mode 100644 internal/api/authz/access_token.go rename internal/api/authz/{token_test.go => access_token_test.go} (65%) create mode 100644 internal/api/authz/api_token_verifier.go create mode 100644 internal/api/authz/membertype_enumer.go create mode 100644 internal/api/authz/session_token.go create mode 100644 internal/api/authz/system_token.go delete mode 100644 internal/api/authz/token.go rename internal/config/hook/{feature.go => enum.go} (54%) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 4baf021b9e..71908f7853 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -389,11 +389,27 @@ EncryptionKeys: UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID SystemAPIUsers: -# Add keys for authentication of the systemAPI here: -# you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT: +# # Add keys for authentication of the systemAPI here: +# # you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT: # - superuser: -# Path: /path/to/superuser/key.pem # you can provide the key either by reference with the path +# Path: /path/to/superuser/ey.pem # you can provide the key either by reference with the path +# Memberships: +# # MemberType System allows the user to access all APIs for all instances or organizations +# - MemberType: System +# Roles: +# - "SYSTEM_OWNER" +# # Actually, we don't recommend adding IAM_OWNER and ORG_OWNER to the System membership, as this basically enables god mode for the system user +# - "IAM_OWNER" +# - "ORG_OWNER" +# # MemberType IAM and Organization let you restrict access to a specific instance or organization by specifying the AggregateID +# - MemberType: IAM +# Roles: "IAM_OWNER" +# AggregateID: "123456789012345678" +# - MemberType: Organization +# Roles: "ORG_OWNER" +# AggregateID: "123456789012345678" # - superuser2: +# # If no memberships are specified, the user has a membership of type System with the role "SYSTEM_OWNER" # KeyData: # or you can directly embed it as base64 encoded value #TODO: remove as soon as possible @@ -841,6 +857,29 @@ AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION InternalAuthZ: RolePermissionMappings: + - Role: "SYSTEM_OWNER" + Permissions: + - "system.instance.read" + - "system.instance.write" + - "system.instance.delete" + - "system.domain.read" + - "system.domain.write" + - "system.domain.delete" + - "system.debug.read" + - "system.debug.write" + - "system.debug.delete" + - "system.feature.write" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + - Role: "SYSTEM_OWNER_VIEWER" + Permissions: + - "system.instance.read" + - "system.domain.read" + - "system.debug.read" + - "system.iam.member.read" - Role: "IAM_OWNER" Permissions: - "iam.read" diff --git a/cmd/ready/config.go b/cmd/ready/config.go index b9137519ee..ea4c1290de 100644 --- a/cmd/ready/config.go +++ b/cmd/ready/config.go @@ -7,7 +7,9 @@ import ( "github.com/spf13/viper" "github.com/zitadel/logging" + internal_authz "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/config/hook" + "github.com/zitadel/zitadel/internal/domain" ) type Config struct { @@ -23,7 +25,8 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), + hook.EnumHookFunc(internal_authz.MemberTypeString), )), ) logging.OnError(err).Fatal("unable to read default config") diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 4ff7c0232b..61930baecf 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -15,6 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/query/projection" @@ -45,7 +46,8 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), database.DecodeHook, - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), + hook.EnumHookFunc(authz.MemberTypeString), )), ) logging.OnError(err).Fatal("unable to read default config") @@ -101,7 +103,7 @@ func MustNewSteps(v *viper.Viper) *Steps { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), )), ) logging.OnError(err).Fatal("unable to read steps") diff --git a/cmd/start/config.go b/cmd/start/config.go index 6ef844feed..a682e09efd 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -24,6 +24,7 @@ import ( "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/logstore" @@ -92,7 +93,8 @@ func MustNewConfig(v *viper.Viper) *Config { database.DecodeHook, actions.HTTPConfigDecodeHook, systemAPIUsersDecodeHook, - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), + hook.EnumHookFunc(internal_authz.MemberTypeString), )), ) logging.OnError(err).Fatal("unable to read config") diff --git a/cmd/start/config_test.go b/cmd/start/config_test.go new file mode 100644 index 0000000000..cfbf877ab5 --- /dev/null +++ b/cmd/start/config_test.go @@ -0,0 +1,84 @@ +package start + +import ( + "reflect" + "strings" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/actions" + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" +) + +func TestMustNewConfig(t *testing.T) { + type args struct { + yaml string + } + tests := []struct { + name string + args args + want *Config + }{{ + name: "features ok", + args: args{yaml: ` +DefaultInstance: + Features: + - FeatureLoginDefaultOrg: true +`}, + want: &Config{ + DefaultInstance: command.InstanceSetup{ + Features: map[domain.Feature]any{ + domain.FeatureLoginDefaultOrg: true, + }, + }, + }, + }, { + name: "membership types ok", + args: args{yaml: ` +SystemAPIUsers: +- superuser: + Memberships: + - MemberType: System + - MemberType: Organization + - MemberType: IAM +`}, + want: &Config{ + SystemAPIUsers: map[string]*authz.SystemAPIUser{ + "superuser": { + Memberships: authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + }, { + MemberType: authz.MemberTypeOrganization, + }, { + MemberType: authz.MemberTypeIAM, + }}, + }, + }, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(strings.NewReader(`Log: + Level: info +Actions: + HTTP: + DenyList: [] +` + tt.args.yaml)) + require.NoError(t, err) + tt.want.Log = &logging.Config{Level: "info"} + tt.want.Actions = &actions.Config{HTTP: actions.HTTPConfig{DenyList: []actions.AddressChecker{}}} + require.NoError(t, tt.want.Log.SetLogger()) + got := MustNewConfig(v) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MustNewConfig() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/start/start.go b/cmd/start/start.go index 9da5c114ff..5ed358086b 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -320,8 +320,13 @@ func startAPIs( // always set the origin in the context if available in the http headers, no matter for what protocol router.Use(middleware.OriginHandler) // adds used HTTPPathPattern and RequestMethod to context - router.Use(middleware.ActivityHandler(append(oidcPrefixes, saml.HandlerPrefix, admin.GatewayPathPrefix(), management.GatewayPathPrefix()))) - verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) + router.Use(middleware.ActivityHandler) + systemTokenVerifier, err := internal_authz.StartSystemTokenVerifierFromConfig(http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) + if err != nil { + return err + } + accessTokenVerifer := internal_authz.StartAccessTokenVerifierFromRepo(repo) + verifier := internal_authz.StartAPITokenVerifier(repo, accessTokenVerifer, systemTokenVerifier) tlsConfig, err := config.TLS.Config() if err != nil { return err diff --git a/docs/docs/guides/integrate/access-zitadel-system-api.md b/docs/docs/guides/integrate/access-zitadel-system-api.md index 08bef3656c..821dbc0d3c 100644 --- a/docs/docs/guides/integrate/access-zitadel-system-api.md +++ b/docs/docs/guides/integrate/access-zitadel-system-api.md @@ -49,6 +49,33 @@ SystemAPIUsers: KeyData: ``` +You can define memberships for the user as well: + +```yaml +SystemAPIUsers: + - system-user-1: + Path: /system-user-1.pub + Memberships: + # MemberType System allows the user to access all APIs for all instances or organizations + - MemberType: System + Roles: + - "SYSTEM_OWNER" + - "IAM_OWNER" + - "ORG_OWNER" + # MemberType IAM and Organization let you restrict access to a specific instance or organization by specifying the AggregateID + - MemberType: IAM + Roles: "IAM_OWNER" + AggregateID: "123456789012345678" + - MemberType: Organization + Roles: "ORG_OWNER" + AggregateID: "123456789012345678" + - superuser2: + # If no memberships are specified, the user has a membership of type System with the role "SYSTEM_OWNER" + KeyData: # or you can directly embed it as base64 encoded value +``` + +If you don't specify any memberships, you are allowed to access the whole [ZITADEL System API](/apis/resources/system). + ## Generate JWT Similar to the OAuth 2.0 JWT Profile, we will create and sign a JWT. For this API, the JWT will not be used to authenticate against ZITADEL Authorization Server, but rather directly to the API itself. @@ -145,8 +172,6 @@ You should get a successful response with a `totalResult` number of 1 and the de } ``` -With this token you are allowed to access the whole [ZITADEL System API](/apis/resources/system). - ## Summary * Create an RSA keypair diff --git a/docs/docs/support/advisory/a10007.md b/docs/docs/support/advisory/a10007.md new file mode 100644 index 0000000000..66a9d8eb5a --- /dev/null +++ b/docs/docs/support/advisory/a10007.md @@ -0,0 +1,63 @@ +--- +title: Technical Advisory 10007 +--- + +## Date and Version + +Version: Upcoming + +Date: Upcoming + +## Affected Users + +This advisory applies to self-hosted ZITADEL installations with custom roles to permissions mappings in the *InternalAuthZ.RolePermissionMappings* configuration section. + +## Description + +In upcoming ZITADEL versions, RBAC also applies to [system users defined in the ZITADEL runtime configuration](/guides/integrate/access-zitadel-system-api#runtime-configuration). +This enables fine grained access control to the system API as well as other APIs for system users. +ZITADEL defines the new default roles *SYSTEM_OWNER* and *SYSTEM_OWNER_VIEWER*. +System users without any memberships defined in the configuration will be assigned the *SYSTEM_OWNER* role. +**Self-hosting users who define their own custom mapping at the *InternalAuthZ.RolePermissionMappings* configuration section**, have to define the *SYSTEM_OWNER* role in their configuration too to be able to access the system API with the default system user membership. + +## Statement + +This change is tracked in the following PR: [feat: add SYSTEM_OWNER role](https://github.com/zitadel/zitadel/pull/6765). +As soon as the release version is published, we will include the version here. + +## Mitigation + +If you have a custom role mapping configured, make sure you configure the new role *SYSTEM_OWNER* before migrating to upcoming ZITADEL versions. +As a reference, these are the default mappings: + +```yaml +InternalAuthZ: + RolePermissionMappings: + - Role: "SYSTEM_OWNER" + Permissions: + - "system.instance.read" + - "system.instance.write" + - "system.instance.delete" + - "system.domain.read" + - "system.domain.write" + - "system.domain.delete" + - "system.debug.read" + - "system.debug.write" + - "system.debug.delete" + - "system.feature.write" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + - Role: "SYSTEM_OWNER_VIEWER" + Permissions: + - "system.instance.read" + - "system.domain.read" + - "system.debug.read" +... +``` + +## Impact + +If the system users don't have the correct memberships and roles which resolve to permissions, the system users lose access to the system API. diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index a359b2ee83..28eb02a2d1 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -130,6 +130,18 @@ We understand that these advisories may include breaking changes, and we aim to 2.39.0 Calendar week 41/42 2023 + + + A-10006 + + Additional grant to cockroach database user + Breaking Behaviour Change + + Upcoming Versions require the SYSTEM_OWNER role to be available in the permission role mappings. Self-hosting ZITADEL users who define custom permission role mappings need to make sure their system users don't lose access to the system API. + + Upcoming + Upcoming + ## Subscribe to our Mailing List diff --git a/internal/activity/activity.go b/internal/activity/activity.go index 4ceb681cf7..2bea722414 100644 --- a/internal/activity/activity.go +++ b/internal/activity/activity.go @@ -45,29 +45,47 @@ func (t TriggerMethod) String() string { } func Trigger(ctx context.Context, orgID, userID string, trigger TriggerMethod) { - triggerLog(authz.GetInstance(ctx).InstanceID(), orgID, userID, http_utils.ComposedOrigin(ctx), trigger, info.ActivityInfoFromContext(ctx)) + ai := info.ActivityInfoFromContext(ctx) + triggerLog( + authz.GetInstance(ctx).InstanceID(), + orgID, + userID, + http_utils.ComposedOrigin(ctx), + trigger, + ai.Method, + ai.Path, + ai.RequestMethod, + authz.GetCtxData(ctx).SystemMemberships != nil, + ) } func TriggerWithContext(ctx context.Context, trigger TriggerMethod) { - data := authz.GetCtxData(ctx) ai := info.ActivityInfoFromContext(ctx) - // if GRPC call, path is prefilled with the grpc fullmethod and method is empty - if ai.Method == "" { - ai.Method = ai.Path - ai.Path = "" - } - triggerLog(authz.GetInstance(ctx).InstanceID(), data.OrgID, data.UserID, http_utils.ComposedOrigin(ctx), trigger, ai) + // GRPC call the method is contained in the HTTP request path + method := ai.Path + triggerLog( + authz.GetInstance(ctx).InstanceID(), + authz.GetCtxData(ctx).OrgID, + authz.GetCtxData(ctx).UserID, + http_utils.ComposedOrigin(ctx), + trigger, + method, + "", + ai.RequestMethod, + authz.GetCtxData(ctx).SystemMemberships != nil, + ) } -func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, ai *info.ActivityInfo) { +func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, method, path, requestMethod string, isSystemUser bool) { logging.WithFields( "instance", instanceID, "org", orgID, "user", userID, "domain", domain, "trigger", trigger.String(), - "method", ai.Method, - "path", ai.Path, - "requestMethod", ai.RequestMethod, + "method", method, + "path", path, + "requestMethod", requestMethod, + "isSystemUser", isSystemUser, ).Info(Activity) } diff --git a/internal/api/api.go b/internal/api/api.go index 9952863eb7..aaa3cd66d3 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -28,7 +28,7 @@ import ( type API struct { port uint16 grpcServer *grpc.Server - verifier *internal_authz.TokenVerifier + verifier internal_authz.APITokenVerifier health healthCheck router *mux.Router http1HostName string @@ -47,7 +47,7 @@ func New( port uint16, router *mux.Router, queries *query.Queries, - verifier *internal_authz.TokenVerifier, + verifier internal_authz.APITokenVerifier, authZ internal_authz.Config, tlsConfig *tls.Config, http2HostName, http1HostName string, accessInterceptor *http_mw.AccessInterceptor, diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 9b73a93e7c..16ec99c41f 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -80,7 +80,7 @@ func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, defa http.Error(w, err.Error(), code) } -func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler { +func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler { h := &Handler{ commands: commands, errorHandler: DefaultErrorHandler, diff --git a/internal/api/authz/access_token.go b/internal/api/authz/access_token.go new file mode 100644 index 0000000000..de7634c36d --- /dev/null +++ b/internal/api/authz/access_token.go @@ -0,0 +1,44 @@ +package authz + +import ( + "context" + + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +const ( + BearerPrefix = "Bearer " +) + +type MembershipsResolver interface { + SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) +} + +type authZRepo interface { + MembershipsResolver + VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error) + VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error) + ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) + ExistsOrg(ctx context.Context, id, domain string) (string, error) +} + +var _ AccessTokenVerifier = (*AccessTokenVerifierFromRepo)(nil) + +type AccessTokenVerifierFromRepo struct { + authZRepo authZRepo +} + +func StartAccessTokenVerifierFromRepo(authZRepo authZRepo) *AccessTokenVerifierFromRepo { + return &AccessTokenVerifierFromRepo{authZRepo: authZRepo} +} + +func (a *AccessTokenVerifierFromRepo) VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + userID, agentID, clientID, prefLang, resourceOwner, err = a.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID()) + return userID, clientID, agentID, prefLang, resourceOwner, err +} + +type client struct { + name string +} diff --git a/internal/api/authz/token_test.go b/internal/api/authz/access_token_test.go similarity index 65% rename from internal/api/authz/token_test.go rename to internal/api/authz/access_token_test.go index 04e104ec2b..54f3c6518c 100644 --- a/internal/api/authz/token_test.go +++ b/internal/api/authz/access_token_test.go @@ -2,19 +2,17 @@ package authz import ( "context" - "sync" "testing" "github.com/zitadel/zitadel/internal/errors" ) -func Test_VerifyAccessToken(t *testing.T) { +func Test_extractBearerToken(t *testing.T) { type args struct { ctx context.Context token string - verifier *TokenVerifier - method string + verifier AccessTokenVerifier } tests := []struct { name string @@ -42,23 +40,16 @@ func Test_VerifyAccessToken(t *testing.T) { args: args{ ctx: context.Background(), token: "Bearer AUTH", - verifier: &TokenVerifier{ - authZRepo: &testVerifier{memberships: []*Membership{}}, - clients: func() sync.Map { - m := sync.Map{} - m.Store("service", &client{name: "name"}) - return m - }(), - authMethods: MethodMapping{"/service/method": Option{Permission: "authenticated"}}, - }, - method: "/service/method", + verifier: AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) { + return "", "", "", "", "", nil + }), }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, _, _, _, _, err := verifyAccessToken(tt.args.ctx, tt.args.token, tt.args.verifier, tt.args.method) + _, err := extractBearerToken(tt.args.token) if tt.wantErr && err == nil { t.Errorf("got wrong result, should get err: actual: %v ", err) } diff --git a/internal/api/authz/api_token_verifier.go b/internal/api/authz/api_token_verifier.go new file mode 100644 index 0000000000..4e25a30aac --- /dev/null +++ b/internal/api/authz/api_token_verifier.go @@ -0,0 +1,69 @@ +package authz + +import ( + "context" + "sync" + + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +// TODO: Define interfaces where they are accepted +type APITokenVerifier interface { + AccessTokenVerifier + SystemTokenVerifier + RegisterServer(appName, methodPrefix string, mappings MethodMapping) + CheckAuthMethod(method string) (Option, bool) + ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) + ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) + SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) +} + +type ApiTokenVerifier struct { + AccessTokenVerifier + SystemTokenVerifier + authZRepo authZRepo + clients sync.Map + authMethods MethodMapping +} + +func StartAPITokenVerifier(authZRepo authZRepo, accessTokenVerifier AccessTokenVerifier, systemTokenVerifier SystemTokenVerifier) *ApiTokenVerifier { + return &ApiTokenVerifier{ + authZRepo: authZRepo, + SystemTokenVerifier: systemTokenVerifier, + AccessTokenVerifier: accessTokenVerifier, + } +} + +func (v *ApiTokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) { + v.clients.Store(methodPrefix, &client{name: appName}) + if v.authMethods == nil { + v.authMethods = make(map[string]Option) + } + for method, option := range mappings { + v.authMethods[method] = option + } +} + +func (v *ApiTokenVerifier) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + return v.authZRepo.SearchMyMemberships(ctx, orgID, shouldTriggerBulk) +} + +func (v *ApiTokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID) +} + +func (v *ApiTokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + return v.authZRepo.ExistsOrg(ctx, id, domain) +} + +func (v *ApiTokenVerifier) CheckAuthMethod(method string) (Option, bool) { + authOpt, ok := v.authMethods[method] + return authOpt, ok +} diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index 2db8f14ad4..ad55ab976c 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -19,11 +19,11 @@ const ( // - the organisation (**either** provided by ID or verified domain) exists // - the user is permitted to call the requested endpoint (permission option in proto) // it will pass the [CtxData] and permission of the user into the ctx [context.Context] -func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { +func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { ctx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() - ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, method) + ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier) if err != nil { return nil, err } diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index a916dfcc8a..0aa22943dd 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -1,12 +1,15 @@ +//go:generate enumer -type MemberType -trimprefix MemberType + package authz import ( "context" + "errors" "strings" "github.com/zitadel/zitadel/internal/api/grpc" http_util "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/errors" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/telemetry/tracing" ) @@ -26,10 +29,11 @@ type CtxData struct { AgentID string PreferredLanguage string ResourceOwner string + SystemMemberships Memberships } func (ctxData CtxData) IsZero() bool { - return ctxData.UserID == "" || ctxData.OrgID == "" + return ctxData.UserID == "" || ctxData.OrgID == "" && ctxData.SystemMemberships == nil } type Grants []*Grant @@ -54,29 +58,68 @@ type MemberType int32 const ( MemberTypeUnspecified MemberType = iota - MemberTypeOrganisation + MemberTypeOrganization MemberTypeProject MemberTypeProjectGrant - MemberTypeIam + MemberTypeIAM + MemberTypeSystem ) -func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t *TokenVerifier, method string) (_ CtxData, err error) { +type TokenVerifier interface { + ExistsOrg(ctx context.Context, id, domain string) (string, error) + ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) + AccessTokenVerifier + SystemTokenVerifier +} + +type AccessTokenVerifier interface { + VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLan, resourceOwner string, err error) +} + +// AccessTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a AccessTokenVerifier. +type AccessTokenVerifierFunc func(context.Context, string) (string, string, string, string, string, error) + +func (a AccessTokenVerifierFunc) VerifyAccessToken(ctx context.Context, token string) (string, string, string, string, string, error) { + return a(ctx, token) +} + +type SystemTokenVerifier interface { + VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error) +} + +// SystemTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a SystemTokenVerifier. +type SystemTokenVerifierFunc func(context.Context, string, string) (Memberships, string, error) + +func (s SystemTokenVerifierFunc) VerifySystemToken(ctx context.Context, token string, orgID string) (Memberships, string, error) { + return s(ctx, token, orgID) +} + +func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier) (_ CtxData, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - - userID, clientID, agentID, prefLang, resourceOwner, err := verifyAccessToken(ctx, token, t, method) + tokenWOBearer, err := extractBearerToken(token) if err != nil { return CtxData{}, err } - if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") { - return CtxData{UserID: userID}, nil + userID, clientID, agentID, prefLang, resourceOwner, err := t.VerifyAccessToken(ctx, tokenWOBearer) + var sysMemberships Memberships + if err != nil && !zitadel_errors.IsUnauthenticated(err) { + return CtxData{}, err + } + if err != nil { + var sysTokenErr error + sysMemberships, userID, sysTokenErr = t.VerifySystemToken(ctx, tokenWOBearer, orgID) + err = errors.Join(err, sysTokenErr) + if sysTokenErr != nil || sysMemberships == nil { + return CtxData{}, err + } } var projectID string var origins []string if clientID != "" { projectID, origins, err = t.ProjectIDAndOriginsByClientID(ctx, clientID) if err != nil { - return CtxData{}, errors.ThrowPermissionDenied(err, "AUTH-GHpw2", "could not read projectid by clientid") + return CtxData{}, zitadel_errors.ThrowPermissionDenied(err, "AUTH-GHpw2", "could not read projectid by clientid") } // We used to check origins for every token, but service users shouldn't be used publicly (native app / SPA). // Therefore, mostly won't send an origin and aren't able to configure them anyway. @@ -88,21 +131,22 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain st if orgID == "" && orgDomain == "" { orgID = resourceOwner } - - verifiedOrgID, err := t.ExistsOrg(ctx, orgID, orgDomain) - if err != nil { - return CtxData{}, errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist") + // System API calls dont't have a resource owner + if orgID != "" { + orgID, err = t.ExistsOrg(ctx, orgID, orgDomain) + if err != nil { + return CtxData{}, zitadel_errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist") + } } - return CtxData{ UserID: userID, - OrgID: verifiedOrgID, + OrgID: orgID, ProjectID: projectID, AgentID: agentID, PreferredLanguage: prefLang, ResourceOwner: resourceOwner, + SystemMemberships: sysMemberships, }, nil - } func SetCtxData(ctx context.Context, ctxData CtxData) context.Context { @@ -119,11 +163,6 @@ func GetRequestPermissionsFromCtx(ctx context.Context) []string { return ctxPermission } -func GetAllPermissionsFromCtx(ctx context.Context) []string { - ctxPermission, _ := ctx.Value(allPermissionsKey).([]string) - return ctxPermission -} - func checkOrigin(ctx context.Context, origins []string) error { origin := grpc.GetGatewayHeader(ctx, http_util.Origin) if origin == "" { @@ -135,5 +174,13 @@ func checkOrigin(ctx context.Context, origins []string) error { if http_util.IsOriginAllowed(origins, origin) { return nil } - return errors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed") + return zitadel_errors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed") +} + +func extractBearerToken(token string) (part string, err error) { + parts := strings.Split(token, BearerPrefix) + if len(parts) != 2 { + return "", zitadel_errors.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header") + } + return parts[1], nil } diff --git a/internal/api/authz/membertype_enumer.go b/internal/api/authz/membertype_enumer.go new file mode 100644 index 0000000000..5de4c92292 --- /dev/null +++ b/internal/api/authz/membertype_enumer.go @@ -0,0 +1,94 @@ +// Code generated by "enumer -type MemberType -trimprefix MemberType"; DO NOT EDIT. + +package authz + +import ( + "fmt" + "strings" +) + +const _MemberTypeName = "UnspecifiedOrganizationProjectProjectGrantIAMSystem" + +var _MemberTypeIndex = [...]uint8{0, 11, 23, 30, 42, 45, 51} + +const _MemberTypeLowerName = "unspecifiedorganizationprojectprojectgrantiamsystem" + +func (i MemberType) String() string { + if i < 0 || i >= MemberType(len(_MemberTypeIndex)-1) { + return fmt.Sprintf("MemberType(%d)", i) + } + return _MemberTypeName[_MemberTypeIndex[i]:_MemberTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _MemberTypeNoOp() { + var x [1]struct{} + _ = x[MemberTypeUnspecified-(0)] + _ = x[MemberTypeOrganization-(1)] + _ = x[MemberTypeProject-(2)] + _ = x[MemberTypeProjectGrant-(3)] + _ = x[MemberTypeIAM-(4)] + _ = x[MemberTypeSystem-(5)] +} + +var _MemberTypeValues = []MemberType{MemberTypeUnspecified, MemberTypeOrganization, MemberTypeProject, MemberTypeProjectGrant, MemberTypeIAM, MemberTypeSystem} + +var _MemberTypeNameToValueMap = map[string]MemberType{ + _MemberTypeName[0:11]: MemberTypeUnspecified, + _MemberTypeLowerName[0:11]: MemberTypeUnspecified, + _MemberTypeName[11:23]: MemberTypeOrganization, + _MemberTypeLowerName[11:23]: MemberTypeOrganization, + _MemberTypeName[23:30]: MemberTypeProject, + _MemberTypeLowerName[23:30]: MemberTypeProject, + _MemberTypeName[30:42]: MemberTypeProjectGrant, + _MemberTypeLowerName[30:42]: MemberTypeProjectGrant, + _MemberTypeName[42:45]: MemberTypeIAM, + _MemberTypeLowerName[42:45]: MemberTypeIAM, + _MemberTypeName[45:51]: MemberTypeSystem, + _MemberTypeLowerName[45:51]: MemberTypeSystem, +} + +var _MemberTypeNames = []string{ + _MemberTypeName[0:11], + _MemberTypeName[11:23], + _MemberTypeName[23:30], + _MemberTypeName[30:42], + _MemberTypeName[42:45], + _MemberTypeName[45:51], +} + +// MemberTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func MemberTypeString(s string) (MemberType, error) { + if val, ok := _MemberTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _MemberTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to MemberType values", s) +} + +// MemberTypeValues returns all values of the enum +func MemberTypeValues() []MemberType { + return _MemberTypeValues +} + +// MemberTypeStrings returns a slice of all String values of the enum +func MemberTypeStrings() []string { + strs := make([]string, len(_MemberTypeNames)) + copy(strs, _MemberTypeNames) + return strs +} + +// IsAMemberType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i MemberType) IsAMemberType() bool { + for _, v := range _MemberTypeValues { + if i == v { + return true + } + } + return false +} diff --git a/internal/api/authz/permissions.go b/internal/api/authz/permissions.go index a7f26b2d0b..8400efe8ff 100644 --- a/internal/api/authz/permissions.go +++ b/internal/api/authz/permissions.go @@ -30,6 +30,11 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi return nil, nil, errors.ThrowUnauthenticated(nil, "AUTH-rKLWEH", "context missing") } + if ctxData.SystemMemberships != nil { + requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings) + return requestedPermissions, allPermissions, nil + } + ctx = context.WithValue(ctx, dataKey, ctxData) memberships, err := resolver.SearchMyMemberships(ctx, orgID, false) if err != nil { diff --git a/internal/api/authz/permissions_test.go b/internal/api/authz/permissions_test.go index 41b1ff2b8f..a500f6a4fb 100644 --- a/internal/api/authz/permissions_test.go +++ b/internal/api/authz/permissions_test.go @@ -7,33 +7,6 @@ import ( caos_errs "github.com/zitadel/zitadel/internal/errors" ) -func getTestCtx(userID, orgID string) context.Context { - return context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID}) -} - -type testVerifier struct { - memberships []*Membership -} - -func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { - return "userID", "agentID", "clientID", "de", "orgID", nil -} -func (v *testVerifier) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*Membership, error) { - return v.memberships, nil -} - -func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) { - return "", nil, nil -} - -func (v *testVerifier) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { - return orgID, nil -} - -func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, string, error) { - return "clientID", "projectID", nil -} - func equalStringArray(a, b []string) bool { if len(a) != len(b) { return false @@ -46,12 +19,18 @@ func equalStringArray(a, b []string) bool { return true } +type membershipsResolverFunc func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) + +func (m membershipsResolverFunc) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) { + return m(ctx, orgID, shouldTriggerBulk) +} + func Test_GetUserPermissions(t *testing.T) { type args struct { - ctxData CtxData - verifier *TokenVerifier - requiredPerm string - authConfig Config + ctxData CtxData + membershipsResolver MembershipsResolver + requiredPerm string + authConfig Config } tests := []struct { name string @@ -64,11 +43,9 @@ func Test_GetUserPermissions(t *testing.T) { name: "Empty Context", args: args{ ctxData: CtxData{}, - verifier: Start(&testVerifier{memberships: []*Membership{ - { - Roles: []string{"ORG_OWNER"}, - }, - }}, "", nil), + membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) { + return []*Membership{{Roles: []string{"ORG_OWNER"}}}, nil + }), requiredPerm: "project.read", authConfig: Config{ RolePermissionMappings: []RoleMapping{ @@ -90,8 +67,10 @@ func Test_GetUserPermissions(t *testing.T) { { name: "No Grants", args: args{ - ctxData: CtxData{}, - verifier: Start(&testVerifier{memberships: []*Membership{}}, "", nil), + ctxData: CtxData{}, + membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) { + return []*Membership{}, nil + }), requiredPerm: "project.read", authConfig: Config{ RolePermissionMappings: []RoleMapping{ @@ -112,14 +91,16 @@ func Test_GetUserPermissions(t *testing.T) { name: "Get Permissions", args: args{ ctxData: CtxData{UserID: "userID", OrgID: "orgID"}, - verifier: Start(&testVerifier{memberships: []*Membership{ - { - AggregateID: "IAM", - ObjectID: "IAM", - MemberType: MemberTypeIam, - Roles: []string{"IAM_OWNER"}, - }, - }}, "", nil), + membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) { + return []*Membership{ + { + AggregateID: "IAM", + ObjectID: "IAM", + MemberType: MemberTypeIAM, + Roles: []string{"IAM_OWNER"}, + }, + }, nil + }), requiredPerm: "project.read", authConfig: Config{ RolePermissionMappings: []RoleMapping{ @@ -139,7 +120,7 @@ func Test_GetUserPermissions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, perms, err := getUserPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID) + _, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID) if tt.wantErr && err == nil { t.Errorf("got wrong result, should get err: actual: %v ", err) @@ -176,7 +157,7 @@ func Test_MapMembershipToPermissions(t *testing.T) { { AggregateID: "1", ObjectID: "1", - MemberType: MemberTypeOrganisation, + MemberType: MemberTypeOrganization, Roles: []string{"ORG_OWNER"}, }, }, @@ -204,7 +185,7 @@ func Test_MapMembershipToPermissions(t *testing.T) { { AggregateID: "1", ObjectID: "1", - MemberType: MemberTypeOrganisation, + MemberType: MemberTypeOrganization, Roles: []string{"ORG_OWNER"}, }, }, @@ -232,13 +213,13 @@ func Test_MapMembershipToPermissions(t *testing.T) { { AggregateID: "1", ObjectID: "1", - MemberType: MemberTypeOrganisation, + MemberType: MemberTypeOrganization, Roles: []string{"ORG_OWNER"}, }, { AggregateID: "IAM", ObjectID: "IAM", - MemberType: MemberTypeIam, + MemberType: MemberTypeIAM, Roles: []string{"IAM_OWNER"}, }, }, @@ -266,7 +247,7 @@ func Test_MapMembershipToPermissions(t *testing.T) { { AggregateID: "2", ObjectID: "2", - MemberType: MemberTypeOrganisation, + MemberType: MemberTypeOrganization, Roles: []string{"ORG_OWNER"}, }, { @@ -327,7 +308,7 @@ func Test_MapMembershipToPerm(t *testing.T) { membership: &Membership{ AggregateID: "Org", ObjectID: "Org", - MemberType: MemberTypeOrganisation, + MemberType: MemberTypeOrganization, Roles: []string{"ORG_OWNER"}, }, authConfig: Config{ @@ -355,7 +336,7 @@ func Test_MapMembershipToPerm(t *testing.T) { membership: &Membership{ AggregateID: "Org", ObjectID: "Org", - MemberType: MemberTypeOrganisation, + MemberType: MemberTypeOrganization, Roles: []string{"ORG_OWNER"}, }, authConfig: Config{ diff --git a/internal/api/authz/session_token.go b/internal/api/authz/session_token.go new file mode 100644 index 0000000000..1691828513 --- /dev/null +++ b/internal/api/authz/session_token.go @@ -0,0 +1,32 @@ +package authz + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/zitadel/zitadel/internal/crypto" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +const ( + SessionTokenPrefix = "sess_" + SessionTokenFormat = SessionTokenPrefix + "%s:%s" +) + +func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { + return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { + decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken) + if err != nil { + return err + } + _, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash") + token, err := algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID()) + spanPasswordComparison.EndWithError(err) + if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) { + return zitadel_errors.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid") + } + return nil + } +} diff --git a/internal/api/authz/system_token.go b/internal/api/authz/system_token.go new file mode 100644 index 0000000000..08d7bde664 --- /dev/null +++ b/internal/api/authz/system_token.go @@ -0,0 +1,117 @@ +package authz + +import ( + "context" + "crypto/rsa" + "errors" + "os" + "sync" + "time" + + "github.com/go-jose/go-jose/v3" + "github.com/zitadel/oidc/v3/pkg/op" + + "github.com/zitadel/zitadel/internal/crypto" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" +) + +var _ SystemTokenVerifier = (*SystemTokenVerifierFromConfig)(nil) + +type SystemTokenVerifierFromConfig struct { + systemJWTProfile *op.JWTProfileVerifier + systemUsers map[string]Memberships +} + +func StartSystemTokenVerifierFromConfig(issuer string, keys map[string]*SystemAPIUser) (*SystemTokenVerifierFromConfig, error) { + systemUsers := make(map[string]Memberships, len(keys)) + for userID, key := range keys { + if len(key.Memberships) == 0 { + systemUsers[userID] = Memberships{{MemberType: MemberTypeSystem, Roles: []string{"SYSTEM_OWNER"}}} + continue + } + for _, membership := range key.Memberships { + switch membership.MemberType { + case MemberTypeSystem, MemberTypeIAM, MemberTypeOrganization: + systemUsers[userID] = key.Memberships + case MemberTypeUnspecified, MemberTypeProject, MemberTypeProjectGrant: + return nil, errors.New("for system users, only the membership types System, IAM and Organization are supported") + default: + return nil, errors.New("unknown membership type") + } + } + } + return &SystemTokenVerifierFromConfig{ + systemJWTProfile: op.NewJWTProfileVerifier( + &systemJWTStorage{ + keys: keys, + cachedKeys: make(map[string]*rsa.PublicKey), + }, + issuer, + 1*time.Hour, + time.Second, + ), + systemUsers: systemUsers, + }, nil +} + +func (s *SystemTokenVerifierFromConfig) VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error) { + jwtReq, err := op.VerifyJWTAssertion(ctx, token, s.systemJWTProfile) + if err != nil { + return nil, "", err + } + systemUserMemberships, ok := s.systemUsers[jwtReq.Subject] + if !ok { + return nil, "", zitadel_errors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong") + } + matchingMemberships = make(Memberships, 0, len(systemUserMemberships)) + for _, membership := range systemUserMemberships { + if membership.MemberType == MemberTypeSystem || + membership.MemberType == MemberTypeIAM && GetInstance(ctx).InstanceID() == membership.AggregateID || + membership.MemberType == MemberTypeOrganization && orgID == membership.AggregateID { + matchingMemberships = append(matchingMemberships, membership) + } + } + return matchingMemberships, jwtReq.Subject, nil +} + +type systemJWTStorage struct { + keys map[string]*SystemAPIUser + mutex sync.Mutex + cachedKeys map[string]*rsa.PublicKey +} + +type SystemAPIUser struct { + Path string // if a path is specified, the key will be read from that path + KeyData []byte // else you can also specify the data directly in the KeyData + Memberships Memberships +} + +func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) { + if s.Path != "" { + var err error + s.KeyData, err = os.ReadFile(s.Path) + if err != nil { + return nil, zitadel_errors.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound") + } + } + return crypto.BytesToPublicKey(s.KeyData) +} + +func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) { + cachedKey, ok := s.cachedKeys[userID] + if ok { + return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil + } + key, ok := s.keys[userID] + if !ok { + return nil, zitadel_errors.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound") + } + s.mutex.Lock() + defer s.mutex.Unlock() + publicKey, err := key.readKey() + if err != nil { + return nil, err + } + s.cachedKeys[userID] = publicKey + return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil +} diff --git a/internal/api/authz/token.go b/internal/api/authz/token.go deleted file mode 100644 index e5b34af9a2..0000000000 --- a/internal/api/authz/token.go +++ /dev/null @@ -1,188 +0,0 @@ -package authz - -import ( - "context" - "crypto/rsa" - "encoding/base64" - "fmt" - "os" - "strings" - "sync" - "time" - - "github.com/go-jose/go-jose/v3" - "github.com/zitadel/oidc/v3/pkg/op" - - "github.com/zitadel/zitadel/internal/crypto" - caos_errs "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/telemetry/tracing" -) - -const ( - BearerPrefix = "Bearer " - SessionTokenPrefix = "sess_" - SessionTokenFormat = SessionTokenPrefix + "%s:%s" -) - -type TokenVerifier struct { - authZRepo authZRepo - clients sync.Map - authMethods MethodMapping - systemJWTProfile *op.JWTProfileVerifier -} - -type MembershipsResolver interface { - SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) -} - -type authZRepo interface { - MembershipsResolver - VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error) - VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error) - ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) - ExistsOrg(ctx context.Context, id, domain string) (string, error) -} - -func Start(authZRepo authZRepo, issuer string, keys map[string]*SystemAPIUser) (v *TokenVerifier) { - return &TokenVerifier{ - authZRepo: authZRepo, - systemJWTProfile: op.NewJWTProfileVerifier( - &systemJWTStorage{ - keys: keys, - cachedKeys: make(map[string]*rsa.PublicKey), - }, - issuer, - 1*time.Hour, - time.Second, - ), - } -} - -func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) { - if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") { - userID, err := v.verifySystemToken(ctx, token) - if err != nil { - return "", "", "", "", "", err - } - return userID, "", "", "", "", nil - } - userID, agentID, clientID, prefLang, resourceOwner, err = v.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID()) - return userID, clientID, agentID, prefLang, resourceOwner, err -} - -func (v *TokenVerifier) verifySystemToken(ctx context.Context, token string) (string, error) { - jwtReq, err := op.VerifyJWTAssertion(ctx, token, v.systemJWTProfile) - if err != nil { - return "", err - } - return jwtReq.Subject, nil -} - -type systemJWTStorage struct { - keys map[string]*SystemAPIUser - mutex sync.Mutex - cachedKeys map[string]*rsa.PublicKey -} - -type SystemAPIUser struct { - Path string //if a path is specified, the key will be read from that path - KeyData []byte //else you can also specify the data directly in the KeyData -} - -func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) { - if s.Path != "" { - var err error - s.KeyData, err = os.ReadFile(s.Path) - if err != nil { - return nil, caos_errs.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound") - } - } - return crypto.BytesToPublicKey(s.KeyData) -} - -func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) { - cachedKey, ok := s.cachedKeys[userID] - if ok { - return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil - } - key, ok := s.keys[userID] - if !ok { - return nil, caos_errs.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound") - } - defer s.mutex.Unlock() - s.mutex.Lock() - publicKey, err := key.readKey() - if err != nil { - return nil, err - } - s.cachedKeys[userID] = publicKey - return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil -} - -type client struct { - id string - projectID string - name string -} - -func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) { - v.clients.Store(methodPrefix, &client{name: appName}) - if v.authMethods == nil { - v.authMethods = make(map[string]Option) - } - for method, option := range mappings { - v.authMethods[method] = option - } -} - -func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - return v.authZRepo.SearchMyMemberships(ctx, orgID, shouldTriggerBulk) -} - -func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID) -} - -func (v *TokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - return v.authZRepo.ExistsOrg(ctx, id, domain) -} - -func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) { - authOpt, ok := v.authMethods[method] - return authOpt, ok -} - -func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, method string) (userID, clientID, agentID, prefLan, resourceOwner string, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - parts := strings.Split(token, BearerPrefix) - if len(parts) != 2 { - return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header") - } - return t.VerifyAccessToken(ctx, parts[1], method) -} - -func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) { - decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken) - if err != nil { - return err - } - _, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash") - var token string - token, err = algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID()) - spanPasswordComparison.EndWithError(err) - if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) { - return caos_errs.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid") - } - return nil - } -} diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index 015c8ce83f..44a6172768 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -77,5 +77,9 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc { } func (s *Server) GatewayPathPrefix() string { + return GatewayPathPrefix() +} + +func GatewayPathPrefix() string { return "/auth/v1" } diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index d2a81203ea..21c9d2e726 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -13,13 +13,13 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { return authorize(ctx, req, info, handler, verifier, authConfig) } } -func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier *authz.TokenVerifier, authConfig authz.Config) (_ interface{}, err error) { +func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) { authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod) if !needsToken { return handler(ctx, req) diff --git a/internal/api/grpc/server/middleware/auth_interceptor_test.go b/internal/api/grpc/server/middleware/auth_interceptor_test.go index 6e22d1352b..c644ab2e31 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor_test.go +++ b/internal/api/grpc/server/middleware/auth_interceptor_test.go @@ -2,6 +2,7 @@ package middleware import ( "context" + "errors" "reflect" "testing" @@ -9,44 +10,54 @@ import ( "google.golang.org/grpc/metadata" "github.com/zitadel/zitadel/internal/api/authz" + zitadel_errors "github.com/zitadel/zitadel/internal/errors" ) -var ( - mockMethods = authz.MethodMapping{ - "need.authentication": authz.Option{ - Permission: "authenticated", - }, - } -) +const anAPIRole = "AN_API_ROLE" -type verifierMock struct{} +type authzRepoMock struct{} -func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { +func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "", "", "", "", "", nil } -func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) { - return nil, nil +func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) { + return authz.Memberships{{ + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Roles: []string{anAPIRole}, + }}, nil } -func (v *verifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) { +func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) { return "", nil, nil } -func (v *verifierMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { +func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { return orgID, nil } -func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { +func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { return "", "", nil } +var ( + accessTokenOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) { + return "user1", "", "", "", "org1", nil + }) + accessTokenNOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) { + return "", "", "", "", "", zitadel_errors.ThrowUnauthenticated(nil, "TEST-fQHDI", "unauthenticaded") + }) + systemTokenNOK = authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) { + return nil, "", errors.New("system token error") + }) +) + func Test_authorize(t *testing.T) { type args struct { - ctx context.Context - req interface{} - info *grpc.UnaryServerInfo - handler grpc.UnaryHandler - verifier *authz.TokenVerifier - authConfig authz.Config - authMethods authz.MethodMapping + ctx context.Context + req interface{} + info *grpc.UnaryServerInfo + handler grpc.UnaryHandler + verifier func() authz.APITokenVerifier + authConfig authz.Config } type res struct { want interface{} @@ -64,12 +75,11 @@ func Test_authorize(t *testing.T) { req: &mockReq{}, info: mockInfo("/no/token/needed"), handler: emptyMockHandler, - verifier: func() *authz.TokenVerifier { - verifier := authz.Start(&verifierMock{}, "", nil) + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) verifier.RegisterServer("need", "need", authz.MethodMapping{}) return verifier - }(), - authMethods: mockMethods, + }, }, res{ &mockReq{}, @@ -83,13 +93,12 @@ func Test_authorize(t *testing.T) { req: &mockReq{}, info: mockInfo("/need/authentication"), handler: emptyMockHandler, - verifier: func() *authz.TokenVerifier { - verifier := authz.Start(&verifierMock{}, "", nil) + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}}) return verifier - }(), - authConfig: authz.Config{}, - authMethods: mockMethods, + }, + authConfig: authz.Config{}, }, res{ nil, @@ -103,13 +112,12 @@ func Test_authorize(t *testing.T) { req: &mockReq{}, info: mockInfo("/need/authentication"), handler: emptyMockHandler, - verifier: func() *authz.TokenVerifier { - verifier := authz.Start(&verifierMock{}, "", nil) + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}}) return verifier - }(), - authConfig: authz.Config{}, - authMethods: mockMethods, + }, + authConfig: authz.Config{}, }, res{ nil, @@ -123,13 +131,118 @@ func Test_authorize(t *testing.T) { req: &mockReq{}, info: mockInfo("/need/authentication"), handler: emptyMockHandler, - verifier: func() *authz.TokenVerifier { - verifier := authz.Start(&verifierMock{}, "", nil) + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}}) return verifier - }(), - authConfig: authz.Config{}, - authMethods: mockMethods, + }, + authConfig: authz.Config{}, + }, + res{ + &mockReq{}, + false, + }, + }, + { + "permission denied error", + args{ + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")), + req: &mockReq{}, + info: mockInfo("/need/authentication"), + handler: emptyMockHandler, + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: anAPIRole, + Permissions: []string{"to.do.something.else"}, + }}, + }, + }, + res{ + nil, + true, + }, + }, + { + "permission ok", + args{ + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")), + req: &mockReq{}, + info: mockInfo("/need/authentication"), + handler: emptyMockHandler, + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: anAPIRole, + Permissions: []string{"to.do.something"}, + }}, + }, + }, + res{ + &mockReq{}, + false, + }, + }, + { + "system token permission denied error", + args{ + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")), + req: &mockReq{}, + info: mockInfo("/need/authentication"), + handler: emptyMockHandler, + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) { + return authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + Roles: []string{"A_SYSTEM_ROLE"}, + }}, "systemuser", nil + })) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: "A_SYSTEM_ROLE", + Permissions: []string{"to.do.something.else"}, + }}, + }, + }, + res{ + nil, + true, + }, + }, + { + "system token permission denied error", + args{ + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")), + req: &mockReq{}, + info: mockInfo("/need/authentication"), + handler: emptyMockHandler, + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) { + return authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + Roles: []string{"A_SYSTEM_ROLE"}, + }}, "systemuser", nil + })) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: "A_SYSTEM_ROLE", + Permissions: []string{"to.do.something"}, + }}, + }, }, res{ &mockReq{}, @@ -139,7 +252,7 @@ func Test_authorize(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier, tt.args.authConfig) + got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig) if (err != nil) != tt.res.wantErr { t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr) return diff --git a/internal/api/grpc/server/middleware/quota_interceptor.go b/internal/api/grpc/server/middleware/quota_interceptor.go index cfcdcedb9f..be6e4d6e4d 100644 --- a/internal/api/grpc/server/middleware/quota_interceptor.go +++ b/internal/api/grpc/server/middleware/quota_interceptor.go @@ -28,7 +28,9 @@ func QuotaExhaustedInterceptor(svc *logstore.Service[*record.AccessLog], ignoreS // The auth interceptor will ensure that only authorized or public requests are allowed. // So if there's no authorization context, we don't need to check for limitation - if authz.GetCtxData(ctx).IsZero() { + // Also, we don't limit calls with system user tokens + ctxData := authz.GetCtxData(ctx) + if ctxData.IsZero() || ctxData.SystemMemberships != nil { return handler(ctx, req) } diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 96c8066d1e..2173c36560 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -35,7 +35,7 @@ type WithGatewayPrefix interface { } func CreateServer( - verifier *authz.TokenVerifier, + verifier authz.APITokenVerifier, authConfig authz.Config, queries *query.Queries, hostHeaderName string, diff --git a/internal/api/http/middleware/activity_interceptor.go b/internal/api/http/middleware/activity_interceptor.go index 7cba3db421..7f15a7c319 100644 --- a/internal/api/http/middleware/activity_interceptor.go +++ b/internal/api/http/middleware/activity_interceptor.go @@ -2,31 +2,13 @@ package middleware import ( "net/http" - "strings" "github.com/zitadel/zitadel/internal/api/info" ) -func ActivityHandler(handlerPrefixes []string) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - activityInfo := info.ActivityInfoFromContext(ctx) - hasPrefix := false - // only add path to context if handler is called - for _, prefix := range handlerPrefixes { - if strings.HasPrefix(r.URL.Path, prefix) { - activityInfo.SetPath(r.URL.Path) - hasPrefix = true - break - } - } - // last call is with grpc method as path - if !hasPrefix { - activityInfo.SetMethod(r.URL.Path) - } - ctx = activityInfo.SetRequestMethod(r.Method).IntoContext(ctx) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } +func ActivityHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := info.ActivityInfoFromContext(r.Context()).SetPath(r.URL.Path).SetRequestMethod(r.Method).IntoContext(r.Context()) + next.ServeHTTP(w, r.WithContext(ctx)) + }) } diff --git a/internal/api/http/middleware/auth_interceptor.go b/internal/api/http/middleware/auth_interceptor.go index 047ee77541..c327d8c846 100644 --- a/internal/api/http/middleware/auth_interceptor.go +++ b/internal/api/http/middleware/auth_interceptor.go @@ -11,11 +11,11 @@ import ( ) type AuthInterceptor struct { - verifier *authz.TokenVerifier + verifier authz.APITokenVerifier authConfig authz.Config } -func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) *AuthInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) *AuthInterceptor { return &AuthInterceptor{ verifier: verifier, authConfig: authConfig, @@ -48,7 +48,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc { type httpReq struct{} -func authorize(r *http.Request, verifier *authz.TokenVerifier, authConfig authz.Config) (_ context.Context, err error) { +func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig authz.Config) (_ context.Context, err error) { ctx := r.Context() authOpt, needsToken := verifier.CheckAuthMethod(r.Method + ":" + r.RequestURI) if !needsToken { diff --git a/internal/authz/repository/eventsourcing/eventstore/user_membership.go b/internal/authz/repository/eventsourcing/eventstore/user_membership.go index d4a86a680d..06a67ca39f 100644 --- a/internal/authz/repository/eventsourcing/eventstore/user_membership.go +++ b/internal/authz/repository/eventsourcing/eventstore/user_membership.go @@ -51,7 +51,7 @@ func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context, orgID func userMembershipToMembership(membership *query.Membership) *authz.Membership { if membership.IAM != nil { return &authz.Membership{ - MemberType: authz.MemberTypeIam, + MemberType: authz.MemberTypeIAM, AggregateID: membership.IAM.IAMID, ObjectID: membership.IAM.IAMID, Roles: membership.Roles, @@ -59,7 +59,7 @@ func userMembershipToMembership(membership *query.Membership) *authz.Membership } if membership.Org != nil { return &authz.Membership{ - MemberType: authz.MemberTypeOrganisation, + MemberType: authz.MemberTypeOrganization, AggregateID: membership.Org.OrgID, ObjectID: membership.Org.OrgID, Roles: membership.Roles, diff --git a/internal/config/hook/feature.go b/internal/config/hook/enum.go similarity index 54% rename from internal/config/hook/feature.go rename to internal/config/hook/enum.go index 5eccaa5706..0196a9123e 100644 --- a/internal/config/hook/feature.go +++ b/internal/config/hook/enum.go @@ -4,11 +4,10 @@ import ( "reflect" "github.com/mitchellh/mapstructure" - - "github.com/zitadel/zitadel/internal/domain" + "golang.org/x/exp/constraints" ) -func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType { +func EnumHookFunc[T constraints.Integer](resolve func(string) (T, error)) mapstructure.DecodeHookFuncType { return func( f reflect.Type, t reflect.Type, @@ -17,11 +16,9 @@ func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType { if f.Kind() != reflect.String { return data, nil } - - if t != reflect.TypeOf(domain.FeatureUnspecified) { + if t != reflect.TypeOf(T(0)) { return data, nil } - - return domain.FeatureString(data.(string)) + return resolve(data.(string)) } } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index c7a2faab6f..f750c4d27f 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -293,6 +293,10 @@ service AdminService { post: "/domains/_search"; }; + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Domains"; diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 80fc89076d..01d0aecaf1 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -115,7 +115,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.read"; }; } @@ -126,7 +126,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.read"; }; } @@ -140,7 +140,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.write"; }; } @@ -152,7 +152,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.write"; }; } @@ -165,7 +165,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.write"; }; } @@ -177,12 +177,13 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.delete"; }; } //Returns all instance members matching the request // all queries need to match (ANDed) + // Deprecated: Use the Admin APIs ListIAMMembers instead rpc ListIAMMembers(ListIAMMembersRequest) returns (ListIAMMembersResponse) { option (google.api.http) = { post: "/instances/{instance_id}/members/_search"; @@ -190,11 +191,11 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.iam.member.read"; }; } - // Checks if a domain exists + //Checks if a domain exists rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { option (google.api.http) = { post: "/domains/{domain}/_exists"; @@ -202,11 +203,13 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.read"; }; } // Returns the custom domains of an instance + //Checks if a domain exists + // Deprecated: Use the Admin APIs ListInstanceDomains on the admin API instead rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { option (google.api.http) = { post: "/instances/{instance_id}/domains/_search"; @@ -214,7 +217,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.read"; }; } @@ -226,7 +229,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.write"; }; } @@ -237,7 +240,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.delete"; }; } @@ -249,7 +252,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.write"; }; } @@ -263,7 +266,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.read"; }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -287,7 +290,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.write"; }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -311,7 +314,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.read"; }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -336,9 +339,8 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.delete"; }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "failed events"; responses: { @@ -375,7 +377,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.quota.write"; }; } @@ -392,7 +394,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.quota.write"; }; } @@ -407,7 +409,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.quota.delete"; }; } @@ -419,7 +421,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.feature.write"; }; } From 94cf30c5477bd44255102a26d241de1f8cf8526b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 25 Oct 2023 18:44:05 +0300 Subject: [PATCH 32/48] feat(oidc): use the new oidc server interface (#6779) * feat(oidc): use the new oidc server interface * rename from provider to server * pin logging and oidc packages * use oidc introspection fix branch * add overloaded methods with tracing * cleanup unused code * include latest oidc fixes --------- Co-authored-by: Livio Spring --- cmd/start/start.go | 12 +- go.mod | 6 +- go.sum | 12 +- internal/api/grpc/oidc/v2/oidc.go | 6 +- internal/api/grpc/oidc/v2/server.go | 6 +- internal/api/oidc/op.go | 89 ++++--------- internal/api/oidc/server.go | 188 ++++++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 86 deletions(-) create mode 100644 internal/api/oidc/server.go diff --git a/cmd/start/start.go b/cmd/start/start.go index 5ed358086b..70f19b4e13 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -417,11 +417,11 @@ 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, limitingAccessInterceptor) + oidcServer, err := oidc.NewServer(config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, config.Log.Slog()) if err != nil { return fmt.Errorf("unable to start oidc provider: %w", err) } - apis.RegisterHandlerPrefixes(oidcProvider.HttpHandler(), oidcPrefixes...) + apis.RegisterHandlerPrefixes(oidcServer, oidcPrefixes...) samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, limitingAccessInterceptor) if err != nil { @@ -429,7 +429,7 @@ func startAPIs( } apis.RegisterHandlerOnPrefix(saml.HandlerPrefix, samlProvider.HttpHandler()) - c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal) + c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal) if err != nil { return fmt.Errorf("unable to start console: %w", err) } @@ -442,11 +442,11 @@ func startAPIs( authRepo, store, console.HandlerPrefix+"/", - op.AuthCallbackURL(oidcProvider), + oidcServer.AuthCallbackURL(), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, - op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, + op.NewIssuerInterceptor(oidcServer.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, @@ -463,7 +463,7 @@ func startAPIs( apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix) // After OIDC provider so that the callback endpoint can be used - if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcProvider, config.ExternalSecure)); err != nil { + if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil { return err } // handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes diff --git a/go.mod b/go.mod index 875f6bb0e8..aba6c7fa39 100644 --- a/go.mod +++ b/go.mod @@ -61,8 +61,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 - github.com/zitadel/logging v0.4.0 - github.com/zitadel/oidc/v3 v3.0.2 + github.com/zitadel/logging v0.5.0 + github.com/zitadel/oidc/v3 v3.1.1 github.com/zitadel/passwap v0.4.0 github.com/zitadel/saml v0.1.2 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.43.0 @@ -76,6 +76,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v0.40.0 go.opentelemetry.io/otel/trace v1.19.0 golang.org/x/crypto v0.14.0 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/net v0.17.0 golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.3.0 @@ -110,7 +111,6 @@ require ( github.com/smartystreets/assertions v1.0.0 // indirect github.com/zenazn/goji v1.0.1 // indirect github.com/zitadel/schema v1.3.0 // indirect - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect ) diff --git a/go.sum b/go.sum index 018d31f9ef..74d2dcf168 100644 --- a/go.sum +++ b/go.sum @@ -881,10 +881,10 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/zitadel/logging v0.4.0 h1:lRAIFgaRoJpLNbsL7jtIYHcMDoEJP9QZB4GqMfl4xaA= -github.com/zitadel/logging v0.4.0/go.mod h1:6uALRJawpkkuUPCkgzfgcPR3c2N908wqnOnIrRelUFc= -github.com/zitadel/oidc/v3 v3.0.2 h1:fw0EAjx8lIlDMJ54hDz2fWIhpW/Y13tW5gd1qWGqbr4= -github.com/zitadel/oidc/v3 v3.0.2/go.mod h1:ne9V9FHug4iUZDV42JirWVLHcbmwaxY8LnkcfekHgRg= +github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA= +github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE= +github.com/zitadel/oidc/v3 v3.1.1 h1:6A4j2ynPGuduM0v74zOf27v7m7ehsDtNEWFMw2WZyVs= +github.com/zitadel/oidc/v3 v3.1.1/go.mod h1:ne9V9FHug4iUZDV42JirWVLHcbmwaxY8LnkcfekHgRg= github.com/zitadel/passwap v0.4.0 h1:cMaISx+Ve7ilgG7Q8xOli4Z6IWr8Gndss+jeBk5A3O0= github.com/zitadel/passwap v0.4.0/go.mod h1:yHaDM4A68yRkdic5BZ4iUNoc19hT+kYt8n1/Nz+I87g= github.com/zitadel/saml v0.1.2 h1:RICwNTuP2upX4A1sZ8iq1rv4/x3DhZHzFx1e5bTKoTo= @@ -981,8 +981,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index 465303b98a..6ca77bb619 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -91,7 +91,7 @@ func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae * return nil, err } authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar} - callback, err := oidc.CreateErrorCallbackURL(authReq, errorReasonToOIDC(ae.GetError()), ae.GetErrorDescription(), ae.GetErrorUri(), s.op) + callback, err := oidc.CreateErrorCallbackURL(authReq, errorReasonToOIDC(ae.GetError()), ae.GetErrorDescription(), ae.GetErrorUri(), s.op.Provider()) if err != nil { return nil, err } @@ -110,9 +110,9 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure)) var callback string if aar.ResponseType == domain.OIDCResponseTypeCode { - callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op) + callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider()) } else { - callback, err = oidc.CreateTokenCallbackURL(ctx, authReq, s.op) + callback, err = oidc.CreateTokenCallbackURL(ctx, authReq, s.op.Provider()) } if err != nil { return nil, err diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go index 823594fbd0..7595ae927e 100644 --- a/internal/api/grpc/oidc/v2/server.go +++ b/internal/api/grpc/oidc/v2/server.go @@ -1,11 +1,11 @@ package oidc import ( - "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" @@ -18,7 +18,7 @@ type Server struct { command *command.Commands query *query.Queries - op op.OpenIDProvider + op *oidc.Server externalSecure bool } @@ -27,7 +27,7 @@ type Config struct{} func CreateServer( command *command.Commands, query *query.Queries, - op op.OpenIDProvider, + op *oidc.Server, externalSecure bool, ) *Server { return &Server{ diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index c165b117b9..117172a440 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -9,6 +9,7 @@ import ( "github.com/rakyll/statik/fs" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" + "golang.org/x/exp/slog" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/assets" @@ -80,7 +81,7 @@ type OPStorage struct { assetAPIPrefix func(ctx context.Context) string } -func NewProvider( +func NewServer( config Config, defaultLogoutRedirectURI string, externalSecure bool, @@ -93,19 +94,17 @@ func NewProvider( projections *database.DB, userAgentCookie, instanceHandler func(http.Handler) http.Handler, accessHandler *middleware.AccessInterceptor, -) (op.OpenIDProvider, error) { + fallbackLogger *slog.Logger, +) (*Server, error) { opConfig, err := createOPConfig(config, defaultLogoutRedirectURI, cryptoKey) if err != nil { return nil, caos_errs.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, externalSecure) - options, err := createOptions( - config, - externalSecure, - userAgentCookie, - instanceHandler, - accessHandler.HandleIgnorePathPrefixes(ignoredQuotaLimitEndpoint(config.CustomEndpoints)), - ) + var options []op.Option + if !externalSecure { + options = append(options, op.WithAllowInsecure()) + } if err != nil { return nil, caos_errs.ThrowInternal(err, "OIDC-D3gq1", "cannot create options: %w") } @@ -118,7 +117,22 @@ func NewProvider( if err != nil { return nil, caos_errs.ThrowInternal(err, "OIDC-DAtg3", "cannot create provider") } - return provider, nil + + server := &Server{ + LegacyServer: op.NewLegacyServer(provider, endpoints(config.CustomEndpoints)), + } + metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount} + server.Handler = op.RegisterLegacyServer(server, op.WithHTTPMiddleware( + middleware.MetricsHandler(metricTypes), + middleware.TelemetryHandler(), + middleware.NoCacheInterceptor().Handler, + instanceHandler, + userAgentCookie, + http_utils.CopyHeadersToContext, + accessHandler.HandleIgnorePathPrefixes(ignoredQuotaLimitEndpoint(config.CustomEndpoints)), + )) + + return server, nil } func ignoredQuotaLimitEndpoint(endpoints *EndpointConfig) []string { @@ -158,61 +172,6 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] return opConfig, nil } -func createOptions(config Config, externalSecure bool, userAgentCookie, instanceHandler, accessHandler func(http.Handler) http.Handler) ([]op.Option, error) { - metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount} - options := []op.Option{ - op.WithHttpInterceptors( - middleware.MetricsHandler(metricTypes), - middleware.TelemetryHandler(), - middleware.NoCacheInterceptor().Handler, - instanceHandler, - userAgentCookie, - http_utils.CopyHeadersToContext, - accessHandler, - ), - } - if !externalSecure { - options = append(options, op.WithAllowInsecure()) - } - endpoints := customEndpoints(config.CustomEndpoints) - if len(endpoints) != 0 { - options = append(options, endpoints...) - } - return options, nil -} - -func customEndpoints(endpointConfig *EndpointConfig) []op.Option { - if endpointConfig == nil { - return nil - } - options := []op.Option{} - if endpointConfig.Auth != nil { - options = append(options, op.WithCustomAuthEndpoint(op.NewEndpointWithURL(endpointConfig.Auth.Path, endpointConfig.Auth.URL))) - } - if endpointConfig.Token != nil { - options = append(options, op.WithCustomTokenEndpoint(op.NewEndpointWithURL(endpointConfig.Token.Path, endpointConfig.Token.URL))) - } - if endpointConfig.Introspection != nil { - options = append(options, op.WithCustomIntrospectionEndpoint(op.NewEndpointWithURL(endpointConfig.Introspection.Path, endpointConfig.Introspection.URL))) - } - if endpointConfig.Userinfo != nil { - options = append(options, op.WithCustomUserinfoEndpoint(op.NewEndpointWithURL(endpointConfig.Userinfo.Path, endpointConfig.Userinfo.URL))) - } - if endpointConfig.Revocation != nil { - options = append(options, op.WithCustomRevocationEndpoint(op.NewEndpointWithURL(endpointConfig.Revocation.Path, endpointConfig.Revocation.URL))) - } - if endpointConfig.EndSession != nil { - options = append(options, op.WithCustomEndSessionEndpoint(op.NewEndpointWithURL(endpointConfig.EndSession.Path, endpointConfig.EndSession.URL))) - } - if endpointConfig.Keys != nil { - options = append(options, op.WithCustomKeysEndpoint(op.NewEndpointWithURL(endpointConfig.Keys.Path, endpointConfig.Keys.URL))) - } - if endpointConfig.DeviceAuth != nil { - options = append(options, op.WithCustomDeviceAuthorizationEndpoint(op.NewEndpointWithURL(endpointConfig.DeviceAuth.Path, endpointConfig.DeviceAuth.URL))) - } - return options -} - func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, db *database.DB, externalSecure bool) *OPStorage { return &OPStorage{ repo: repo, diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go new file mode 100644 index 0000000000..f9a38a2613 --- /dev/null +++ b/internal/api/oidc/server.go @@ -0,0 +1,188 @@ +package oidc + +import ( + "context" + "net/http" + + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +type Server struct { + http.Handler + *op.LegacyServer +} + +func endpoints(endpointConfig *EndpointConfig) op.Endpoints { + // some defaults. The new Server will disable enpoints that are nil. + endpoints := op.Endpoints{ + Authorization: op.NewEndpoint("/oauth/v2/authorize"), + Token: op.NewEndpoint("/oauth/v2/token"), + Introspection: op.NewEndpoint("/oauth/v2/introspect"), + Userinfo: op.NewEndpoint("/oidc/v1/userinfo"), + Revocation: op.NewEndpoint("/oauth/v2/revoke"), + EndSession: op.NewEndpoint("/oidc/v1/end_session"), + JwksURI: op.NewEndpoint("/oauth/v2/keys"), + DeviceAuthorization: op.NewEndpoint("/oauth/v2/device_authorization"), + } + + if endpointConfig == nil { + return endpoints + } + if endpointConfig.Auth != nil { + endpoints.Authorization = op.NewEndpointWithURL(endpointConfig.Auth.Path, endpointConfig.Auth.URL) + } + if endpointConfig.Token != nil { + endpoints.Token = op.NewEndpointWithURL(endpointConfig.Token.Path, endpointConfig.Token.URL) + } + if endpointConfig.Introspection != nil { + endpoints.Introspection = op.NewEndpointWithURL(endpointConfig.Introspection.Path, endpointConfig.Introspection.URL) + } + if endpointConfig.Userinfo != nil { + endpoints.Userinfo = op.NewEndpointWithURL(endpointConfig.Userinfo.Path, endpointConfig.Userinfo.URL) + } + if endpointConfig.Revocation != nil { + endpoints.Revocation = op.NewEndpointWithURL(endpointConfig.Revocation.Path, endpointConfig.Revocation.URL) + } + if endpointConfig.EndSession != nil { + endpoints.EndSession = op.NewEndpointWithURL(endpointConfig.EndSession.Path, endpointConfig.EndSession.URL) + } + if endpointConfig.Keys != nil { + endpoints.JwksURI = op.NewEndpointWithURL(endpointConfig.Keys.Path, endpointConfig.Keys.URL) + } + if endpointConfig.DeviceAuth != nil { + endpoints.DeviceAuthorization = op.NewEndpointWithURL(endpointConfig.DeviceAuth.Path, endpointConfig.DeviceAuth.URL) + } + return endpoints +} + +func (s *Server) IssuerFromRequest(r *http.Request) string { + return s.Provider().IssuerFromRequest(r) +} + +func (s *Server) Health(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.Health(ctx, r) +} + +func (s *Server) Ready(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.Ready(ctx, r) +} + +func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.Discovery(ctx, r) +} + +func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.Keys(ctx, r) +} + +func (s *Server) VerifyAuthRequest(ctx context.Context, r *op.Request[oidc.AuthRequest]) (_ *op.ClientRequest[oidc.AuthRequest], err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.VerifyAuthRequest(ctx, r) +} + +func (s *Server) Authorize(ctx context.Context, r *op.ClientRequest[oidc.AuthRequest]) (_ *op.Redirect, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.Authorize(ctx, r) +} + +func (s *Server) DeviceAuthorization(ctx context.Context, r *op.ClientRequest[oidc.DeviceAuthorizationRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.DeviceAuthorization(ctx, r) +} + +func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.VerifyClient(ctx, r) +} + +func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.AccessTokenRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.CodeExchange(ctx, r) +} + +func (s *Server) RefreshToken(ctx context.Context, r *op.ClientRequest[oidc.RefreshTokenRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.RefreshToken(ctx, r) +} + +func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGrantRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.JWTProfile(ctx, r) +} + +func (s *Server) TokenExchange(ctx context.Context, r *op.ClientRequest[oidc.TokenExchangeRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.TokenExchange(ctx, r) +} + +func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequest[oidc.ClientCredentialsRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.ClientCredentialsExchange(ctx, r) +} + +func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.DeviceAccessTokenRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.DeviceToken(ctx, r) +} + +func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.Introspect(ctx, r) +} + +func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.UserInfo(ctx, r) +} + +func (s *Server) Revocation(ctx context.Context, r *op.ClientRequest[oidc.RevocationRequest]) (_ *op.Response, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.Revocation(ctx, r) +} + +func (s *Server) EndSession(ctx context.Context, r *op.Request[oidc.EndSessionRequest]) (_ *op.Redirect, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + return s.LegacyServer.EndSession(ctx, r) +} From b51ad53e5a8a6b884e0b2dcc06a68dca9ec8b5eb Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:05:00 +0200 Subject: [PATCH 33/48] fix: list mapping of saml provider configuration (#6804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tim Möhlmann --- internal/api/grpc/idp/converter.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index f3b0f50779..e0b0b6fb40 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -1,6 +1,7 @@ package idp import ( + "github.com/crewjam/saml" "google.golang.org/protobuf/types/known/durationpb" obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object" @@ -476,6 +477,10 @@ func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig { appleConfigToPb(providerConfig, config.AppleIDPTemplate) return providerConfig } + if config.SAMLIDPTemplate != nil { + samlConfigToPb(providerConfig, config.SAMLIDPTemplate) + return providerConfig + } return providerConfig } @@ -637,3 +642,28 @@ func appleConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.Appl }, } } + +func samlConfigToPb(providerConfig *idp_pb.ProviderConfig, template *query.SAMLIDPTemplate) { + providerConfig.Config = &idp_pb.ProviderConfig_Saml{ + Saml: &idp_pb.SAMLConfig{ + MetadataXml: template.Metadata, + Binding: bindingToPb(template.Binding), + WithSignedRequest: template.WithSignedRequest, + }, + } +} + +func bindingToPb(binding string) idp_pb.SAMLBinding { + switch binding { + case "": + return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED + case saml.HTTPPostBinding: + return idp_pb.SAMLBinding_SAML_BINDING_POST + case saml.HTTPRedirectBinding: + return idp_pb.SAMLBinding_SAML_BINDING_REDIRECT + case saml.HTTPArtifactBinding: + return idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT + default: + return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED + } +} From 9d77dcb467c5fe56e1192ac68d56d794b7baf0f2 Mon Sep 17 00:00:00 2001 From: Miguel Cabrerizo <30386061+doncicuto@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:39:28 +0200 Subject: [PATCH 34/48] feat(saml): option to create minimal SAML metadata file (#6671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: initial look and feel * feat: initial textarea * feat: app details and i18n * fix: add @peintnermax suggestions * fix: detail component move code to valueChanges.subscribe and clear inputs if metadataurl set --------- Co-authored-by: Max Peintner Co-authored-by: Elio Bischof Co-authored-by: Fabi Co-authored-by: Tim Möhlmann --- .../apps/app-create/app-create.component.html | 132 +++++++++++++----- .../apps/app-create/app-create.component.scss | 21 +++ .../apps/app-create/app-create.component.ts | 27 ++++ .../apps/app-detail/app-detail.component.html | 41 ++++-- .../apps/app-detail/app-detail.component.scss | 21 +++ .../apps/app-detail/app-detail.component.ts | 37 ++++- console/src/assets/i18n/bg.json | 13 +- console/src/assets/i18n/de.json | 13 +- console/src/assets/i18n/en.json | 13 +- console/src/assets/i18n/es.json | 13 +- console/src/assets/i18n/fr.json | 17 +++ console/src/assets/i18n/it.json | 13 +- console/src/assets/i18n/ja.json | 13 +- console/src/assets/i18n/mk.json | 15 +- console/src/assets/i18n/pl.json | 13 +- console/src/assets/i18n/pt.json | 13 +- console/src/assets/i18n/zh.json | 17 +++ 17 files changed, 349 insertions(+), 83 deletions(-) diff --git a/console/src/app/pages/projects/apps/app-create/app-create.component.html b/console/src/app/pages/projects/apps/app-create/app-create.component.html index 1603c8f271..04a1da6d5a 100644 --- a/console/src/app/pages/projects/apps/app-create/app-create.component.html +++ b/console/src/app/pages/projects/apps/app-create/app-create.component.html @@ -170,26 +170,56 @@ {{ 'APP.SAML.CONFIGSECTION' | translate }} -
- - {{ 'APP.SAML.URL' | translate }} - - -
+

+ {{ 'APP.SAML.CHOOSEMETADATASOURCE' | translate }} +

- {{ 'APP.SAML.OR' | translate }} + - - + + +
PREVIEW

-->
- - {{ 'APP.SAML.URL' | translate }} - - +

+ {{ 'APP.SAML.CHOOSEMETADATASOURCE' | translate }} +

+ - {{ 'APP.SAML.OR' | translate }} + + +
+ + + + +` + : ''; + if (form.metadataUrl && form.metadataUrl.length > 0) { this.samlAppRequest.setMetadataUrl(form.metadataUrl); } + + if (this.samlAppRequest) { + const base64 = Buffer.from(minimalMetadata, 'utf-8').toString('base64'); + this.samlAppRequest.setMetadataXml(base64); + } }); } @@ -352,6 +369,8 @@ export class AppCreateComponent implements OnInit, OnDestroy { public onDropXML(filelist: FileList): void { const file = filelist.item(0); this.metadataUrl?.setValue(''); + this.entityId?.setValue(''); + this.acsURL?.setValue(''); if (file) { if (file.size > MAX_ALLOWED_SIZE) { this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true); @@ -547,4 +566,12 @@ export class AppCreateComponent implements OnInit, OnDestroy { public get metadataUrl(): AbstractControl | null { return this.samlConfigForm.get('metadataUrl'); } + + public get entityId(): AbstractControl | null { + return this.samlConfigForm.get('entityId'); + } + + public get acsURL(): AbstractControl | null { + return this.samlConfigForm.get('acsURL'); + } } diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html index 18eff90897..e3884b6166 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html @@ -157,17 +157,22 @@ - -
- - {{ 'APP.SAML.URL' | translate }} - - -
+ +

+ {{ 'APP.SAML.CHOOSEMETADATASOURCE' | translate }} +

-
- {{ 'APP.SAML.OR' | translate }} + + + +
{ + let minimalMetadata = + this.entityId?.value && this.acsURL?.value + ? ` + + + + +` + : ''; + + if (this.metadataUrl && this.metadataUrl.value.length > 0) { + if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml) { + this.app.samlConfig.metadataXml = ''; + } + } + + if (this.app && this.app.samlConfig && this.app.samlConfig.metadataXml && minimalMetadata) { + const base64 = Buffer.from(minimalMetadata, 'utf-8').toString('base64'); + this.app.samlConfig.metadataXml = base64; + } + }); } public formatClockSkewLabel(seconds: number): string { @@ -406,6 +431,8 @@ export class AppDetailComponent implements OnInit, OnDestroy { this.toast.showInfo('POLICY.PRIVATELABELING.MAXSIZEEXCEEDED', true); } else { this.metadataUrl?.setValue(''); + this.entityId?.setValue(''); + this.acsURL?.setValue(''); const reader = new FileReader(); reader.onload = ((aXML) => { return (e) => { @@ -779,6 +806,14 @@ export class AppDetailComponent implements OnInit, OnDestroy { return this.samlForm.get('metadataUrl'); } + public get entityId(): AbstractControl | null { + return this.samlForm.get('entityId'); + } + + public get acsURL(): AbstractControl | null { + return this.samlForm.get('acsURL'); + } + get decodedBase64(): string { if ( this.app && diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 482a479a17..87141b3896 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -2071,11 +2071,16 @@ "DESCRIPTION": "SAML приложения" }, "CONFIGSECTION": "SAML конфигурация", - "URL": "Url, където се намира файлът с метаданни", - "OR": "или", - "XML": "Качете XML метаданни", + "CHOOSEMETADATASOURCE": "Предоставете вашата SAML конфигурация, като използвате една от следните опции:", + "METADATAOPT1": "Опция 1. Посочете URL адреса, където се намира файлът с метаданни", + "METADATAOPT2": "Опция 2. Качете файл, съдържащ вашите XML метаданни", + "METADATAOPT3": "Опция 3. Създайте минимален файл с метаданни в движение, предоставяйки ENTITYID и ACS URL", + "UPLOAD": "Качете XML файл", "METADATA": "Метаданни", - "METADATAFROMFILE": "Метаданни от файл" + "METADATAFROMFILE": "Метаданни от файл", + "CREATEMETADATA": "Създайте метаданни", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index fec4423585..0c67a27ba3 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -2080,11 +2080,16 @@ "DESCRIPTION": "SAML Applikationen" }, "CONFIGSECTION": "SAML Konfiguration", - "URL": "Url der Metadaten-Datei", - "OR": "oder", - "XML": "Metadaten XML hochladen", + "CHOOSEMETADATASOURCE": "Stellen Sie Ihre SAML-Konfiguration mit einer der folgenden Optionen bereit:", + "METADATAOPT1": "Option 1: Geben Sie die URL an, unter der sich die Metadatendatei befindet", + "METADATAOPT2": "Option 2: Laden Sie eine Datei hoch, die Ihre XML-Metadaten enthält", + "METADATAOPT3": "Option 3: Erstellen Sie spontan eine minimale Metadatendatei mit ENTITYID und ACS-URL", + "UPLOAD": "XML-Datei hochladen", "METADATA": "Metadaten", - "METADATAFROMFILE": "Metadata aus Datei" + "METADATAFROMFILE": "Metadata aus Datei", + "CREATEMETADATA": "Create metadata", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index dbcff16583..354b272115 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -2089,11 +2089,16 @@ "DESCRIPTION": "SAML Applications" }, "CONFIGSECTION": "SAML Configuration", - "URL": "Url where Metadata file is located", - "OR": "or", - "XML": "Upload Metadata XML", + "CHOOSEMETADATASOURCE": "Provide your SAML configuration using one of the following options:", + "METADATAOPT1": "Option 1. Specify the url where metadata file is located", + "METADATAOPT2": "Option 2. Upload a file containing your metadata XML", + "METADATAOPT3": "Option 3. Create a minimal metadata file on the fly providing ENTITYID and ACS URL", + "UPLOAD": "Upload XML file", "METADATA": "Metadata", - "METADATAFROMFILE": "Metadata from File" + "METADATAFROMFILE": "Metadata from file", + "CREATEMETADATA": "Create metadata", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index cae80d082c..8475622457 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -2077,11 +2077,16 @@ "DESCRIPTION": "Aplicaciones SAML" }, "CONFIGSECTION": "Configuración SAML", - "URL": "URL donde está ubicado el fichero de metadatos", - "OR": "o", - "XML": "Sube un fichero XML de metadatos", + "CHOOSEMETADATASOURCE": "Proporciona tu configuración SAML usando una de las siguientes opciones:", + "METADATAOPT1": "Opción 1. Especifica la URL donde se encuentra el fichero de metadatos", + "METADATAOPT2": "Opción 2. Sube un fichero que contenga tus metadatos XML", + "METADATAOPT3": "Opción 3. Crea, al vuelo, un fichero de metadatos mínimo proporcionando el ENTITYID y la ACS URL", + "UPLOAD": "Subir fichero XML", "METADATA": "Metadatos", - "METADATAFROMFILE": "Metadatos desde un fichero" + "METADATAFROMFILE": "Metadatos desde un fichero", + "CREATEMETADATA": "Crear metadatos", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 87f94c74c1..0ec28c504d 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -2075,6 +2075,23 @@ "1": "Clé privée JWT" } }, + "SAML": { + "SELECTION": { + "TITLE": "SAML", + "DESCRIPTION": "Applications SAML" + }, + "CONFIGSECTION": "Configuration SAML", + "CHOOSEMETADATASOURCE": "Fournissez votre configuration SAML à l'aide de l'une des options suivantes :", + "METADATAOPT1": "Option 1. Spécifiez l'URL où se trouve le fichier de métadonnées", + "METADATAOPT2": "Option 2. Téléchargez un fichier contenant vos métadonnées XML", + "METADATAOPT3": "Option 3. Créer un fichier de métadonnées minimal à la volée fournissant l'ENTITYID et l'URL ACS", + "UPLOAD": "Télécharger le fichier XML", + "METADATA": "Métadonnées", + "METADATAFROMFILE": "Métadonnées du fichier", + "CREATEMETADATA": "Créer des métadonnées", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" + }, "AUTHMETHODS": { "CODE": { "TITLE": "Code", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 998ea6a662..11150dc8dd 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -2081,11 +2081,16 @@ "DESCRIPTION": "Applicazioni SAMML" }, "CONFIGSECTION": "Configurazione SAML", - "URL": "URL in cui si trova il file di metadati", - "OR": "o", - "XML": "Carica Metadata XML", + "CHOOSEMETADATASOURCE": "Fornisci la tua configurazione SAML utilizzando una delle seguenti opzioni:", + "METADATAOPT1": "Opzione 1. Specificare l'URL in cui si trova il file di metadati", + "METADATAOPT2": "Opzione 2. Carica un file contenente il tuo XML di metadati", + "METADATAOPT3": "Opzione 3. Crea al volo un file di metadati minimo fornendo ENTITYID e URL ACS", + "UPLOAD": "Carica il file XML", "METADATA": "Metadata", - "METADATAFROMFILE": "Metadati dal file" + "METADATAFROMFILE": "Metadati dal file", + "CREATEMETADATA": "Crea metadati", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 69f1d9c1fb..bd5ddecb8d 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -2072,11 +2072,16 @@ "DESCRIPTION": "SAMLアプリケーション" }, "CONFIGSECTION": "SAML構成", - "URL": "メタデータファイルが配置されているURL", - "OR": "または", - "XML": "メタデータXMLをアップロードする", + "CHOOSEMETADATASOURCE": "次のオプションのいずれかを使用して SAML 構成を指定します。", + "METADATAOPT1": "オプション 1. メタデータ ファイルが存在する URL を指定する", + "METADATAOPT2": "オプション 2. メタデータ XML を含むファイルをアップロードする", + "METADATAOPT3": "オプション 3. ENTITYID と ACS URL を指定して最小限のメタデータ ファイルをオンザフライで作成する", + "UPLOAD": "XMLファイルをアップロードする", "METADATA": "メタデータ", - "METADATAFROMFILE": "ファイルからのメタデータ" + "METADATAFROMFILE": "ファイルからのメタデータ", + "CREATEMETADATA": "メタデータの作成", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index c152cb14b5..8ce464ddf3 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -2078,11 +2078,16 @@ "DESCRIPTION": "SAML Апликации" }, "CONFIGSECTION": "SAML Конфигурација", - "URL": "URL каде што се наоѓа Metadata датотеката", - "OR": "или", - "XML": "Подигни Metadata XML", - "METADATA": "Metadata", - "METADATAFROMFILE": "Metadata од датотека" + "CHOOSEMETADATASOURCE": "Обезбедете ја вашата SAML конфигурација користејќи една од следниве опции:", + "METADATAOPT1": "Опција 1. Наведете ја адресата каде што се наоѓа датотеката со метаподатоци", + "METADATAOPT2": "Опција 2. Поставете датотека што ги содржи вашите метаподатоци XML", + "METADATAOPT3": "Опција 3. Создадете датотека со минимални метаподатоци во лет, обезбедувајќи ENTITYID и ACS URL", + "UPLOAD": "Поставете XML датотека", + "METADATA": "Метаподатоци", + "METADATAFROMFILE": "Метаподатоци од датотека", + "CREATEMETADATA": "Креирајте метаподатоци", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 02c10021ce..c622e94bd2 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -2081,11 +2081,16 @@ "DESCRIPTION": "Aplikacje SAML" }, "CONFIGSECTION": "Konfiguracja SAML", - "URL": "Adres URL, gdzie znajduje się plik metadanych", - "OR": "lub", - "XML": "Prześlij plik XML metadanych", + "CHOOSEMETADATASOURCE": "Podaj konfigurację SAML, korzystając z jednej z następujących opcji:", + "METADATAOPT1": "Opcja 1. Podaj adres URL, pod którym znajduje się plik metadanych", + "METADATAOPT2": "Opcja 2. Prześlij plik zawierający metadane XML", + "METADATAOPT3": "Opcja 3. Utwórz na bieżąco minimalny plik metadanych, podając ENTITYID i adres URL ACS", + "UPLOAD": "Prześlij plik XML", "METADATA": "Metadane", - "METADATAFROMFILE": "Metadane z pliku" + "METADATAFROMFILE": "Metadane z pliku", + "CREATEMETADATA": "Utwórz metadane", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 07deb19921..47f98ea910 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -2076,11 +2076,16 @@ "DESCRIPTION": "Aplicativos SAML" }, "CONFIGSECTION": "Configuração SAML", - "URL": "URL onde o arquivo de Metadados está localizado", - "OR": "ou", - "XML": "Carregar XML de Metadados", + "CHOOSEMETADATASOURCE": "Forneça sua configuração SAML usando uma das seguintes opções:", + "METADATAOPT1": "Opção 1. Especifique o URL onde o arquivo de metadados está localizado", + "METADATAOPT2": "Opção 2. Faça upload de um arquivo contendo seu XML de metadados", + "METADATAOPT3": "Opção 3. Crie um arquivo mínimo de metadados instantaneamente fornecendo ENTITYID e URL ACS", + "UPLOAD": "Prześlij plik XML", "METADATA": "Metadados", - "METADATAFROMFILE": "Metadados do Arquivo" + "METADATAFROMFILE": "Metadados do Arquivo", + "CREATEMETADATA": "Criar metadados", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" }, "AUTHMETHODS": { "CODE": { diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 626dae19aa..9fac71e227 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -2074,6 +2074,23 @@ "1": "Private Key JWT" } }, + "SAML": { + "SELECTION": { + "TITLE": "SAML", + "DESCRIPTION": "SAML 应用" + }, + "CONFIGSECTION": "SAML 配置", + "CHOOSEMETADATASOURCE": "使用以下选项之一提供您的 SAML 配置:", + "METADATAOPT1": "选项 1.指定元数据文件所在的 url", + "METADATAOPT2": "选项 2. 上传包含元数据 XML 的文件", + "METADATAOPT3": "选项 3. 动态创建最小元数据文件,提供 ENTITYID 和 ACS URL", + "UPLOAD": "上传 XML 文件", + "METADATA": "元数据", + "METADATAFROMFILE": "文件中的元数据", + "CREATEMETADATA": "创建元数据", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" + }, "AUTHMETHODS": { "CODE": { "TITLE": "Code", From b9061ffadcd9adc9ef8314cf356e36f03bcf8d86 Mon Sep 17 00:00:00 2001 From: mffap Date: Wed, 25 Oct 2023 20:15:42 +0200 Subject: [PATCH 35/48] docs(guides): update development mode for console guide (#6799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(guide): development mode * finished --------- Co-authored-by: Fabi Co-authored-by: Livio Spring Co-authored-by: Tim Möhlmann --- .../guides/manage/console/applications.mdx | 11 ++++++++++- .../img/guides/console/developmentmode.png | Bin 0 -> 172503 bytes 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 docs/static/img/guides/console/developmentmode.png diff --git a/docs/docs/guides/manage/console/applications.mdx b/docs/docs/guides/manage/console/applications.mdx index 71285bf97c..70d97cb094 100644 --- a/docs/docs/guides/manage/console/applications.mdx +++ b/docs/docs/guides/manage/console/applications.mdx @@ -112,7 +112,16 @@ Native applications can use a different protocol than http or https in order to width="600px" /> -In order to **develop locally** and due to the fact that any ZITADEL configuration is secure by default, ZITADEL requires you enable **dev mode** if you want to redirect users to URIs other than https://. +### Development mode + +In order to develop locally, or you want to redirect users to any other protocol than https://, you must enable **Development mode**. +When disabled only https is allowed, as ZITADEL's configuration is secure by default. + +Development mode ## Review Configuration diff --git a/docs/static/img/guides/console/developmentmode.png b/docs/static/img/guides/console/developmentmode.png new file mode 100644 index 0000000000000000000000000000000000000000..6bb0c153d61fd59c968e142c080605a72158a1b7 GIT binary patch literal 172503 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 Date: Wed, 25 Oct 2023 22:20:12 +0300 Subject: [PATCH 36/48] feat(system api): list instances by domains (#6806) Allow to list instances by their domains on the system API. closes #6785 --- internal/api/grpc/instance/converter.go | 2 + .../grpc/system/instance_integration_test.go | 109 ++++++++++++++++++ internal/query/instance.go | 12 +- internal/query/instance_test.go | 3 +- proto/zitadel/instance.proto | 12 ++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 internal/api/grpc/system/instance_integration_test.go diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index a59b674b78..ae98bcfa5e 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -63,6 +63,8 @@ func InstanceQueryToModel(searchQuery *instance_pb.Query) (query.SearchQuery, er switch q := searchQuery.Query.(type) { case *instance_pb.Query_IdQuery: return query.NewInstanceIDsListSearchQuery(q.IdQuery.Ids...) + case *instance_pb.Query_DomainQuery: + return query.NewInstanceDomainsListSearchQuery(q.DomainQuery.Domains...) default: return nil, errors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid") } diff --git a/internal/api/grpc/system/instance_integration_test.go b/internal/api/grpc/system/instance_integration_test.go new file mode 100644 index 0000000000..f874ac79f0 --- /dev/null +++ b/internal/api/grpc/system/instance_integration_test.go @@ -0,0 +1,109 @@ +//go:build integration + +package system_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/pkg/grpc/instance" + "github.com/zitadel/zitadel/pkg/grpc/object" + system_pb "github.com/zitadel/zitadel/pkg/grpc/system" +) + +func TestServer_ListInstances(t *testing.T) { + domain, instanceID, _ := Tester.UseIsolatedInstance(CTX, SystemCTX) + + tests := []struct { + name string + req *system_pb.ListInstancesRequest + want []*instance.Instance + wantErr bool + }{ + { + name: "empty query error", + req: &system_pb.ListInstancesRequest{ + Queries: []*instance.Query{{}}, + }, + wantErr: true, + }, + { + name: "non-existing id", + req: &system_pb.ListInstancesRequest{ + Queries: []*instance.Query{{ + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{"foo"}, + }, + }, + }}, + }, + want: []*instance.Instance{}, + }, + { + name: "get 1 by id", + req: &system_pb.ListInstancesRequest{ + Query: &object.ListQuery{ + Limit: 1, + }, + Queries: []*instance.Query{{ + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{instanceID}, + }, + }, + }}, + }, + want: []*instance.Instance{{ + Id: instanceID, + }}, + }, + { + name: "non-existing domain", + req: &system_pb.ListInstancesRequest{ + Queries: []*instance.Query{{ + Query: &instance.Query_DomainQuery{ + DomainQuery: &instance.DomainsQuery{ + Domains: []string{"foo"}, + }, + }, + }}, + }, + want: []*instance.Instance{}, + }, + { + name: "get 1 by domain", + req: &system_pb.ListInstancesRequest{ + Query: &object.ListQuery{ + Limit: 1, + }, + Queries: []*instance.Query{{ + Query: &instance.Query_DomainQuery{ + DomainQuery: &instance.DomainsQuery{ + Domains: []string{domain}, + }, + }, + }}, + }, + want: []*instance.Instance{{ + Id: instanceID, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := Tester.Client.System.ListInstances(SystemCTX, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + got := resp.GetResult() + assert.Len(t, got, len(tt.want)) + for i := 0; i < len(tt.want); i++ { + assert.Equalf(t, tt.want[i].GetId(), got[i].GetId(), "instance[%d] id", i) + } + }) + } +} diff --git a/internal/query/instance.go b/internal/query/instance.go index f570f05912..c80ae9085b 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -150,6 +150,15 @@ func NewInstanceIDsListSearchQuery(ids ...string) (SearchQuery, error) { return NewListQuery(InstanceColumnID, list, ListIn) } +func NewInstanceDomainsListSearchQuery(domains ...string) (SearchQuery, error) { + list := make([]interface{}, len(domains)) + for i, value := range domains { + list[i] = value + } + + return NewListQuery(InstanceDomainDomainCol, list, ListIn) +} + func (q *InstanceSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { @@ -280,7 +289,8 @@ func prepareInstancesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu return sq.Select( InstanceColumnID.identifier(), countColumn.identifier(), - ).From(instanceTable.identifier()), + ).From(instanceTable.identifier()). + LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)), func(builder sq.SelectBuilder) sq.SelectBuilder { return sq.Select( instanceFilterCountColumn, diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index a14b44df0d..27e206e0ee 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -55,7 +55,8 @@ var ( ` projections.instance_domains.creation_date,` + ` projections.instance_domains.change_date, ` + ` projections.instance_domains.sequence` + - ` FROM (SELECT projections.instances.id, COUNT(*) OVER () FROM projections.instances) AS f` + + ` FROM (SELECT projections.instances.id, COUNT(*) OVER () FROM projections.instances` + + ` LEFT JOIN projections.instance_domains ON projections.instances.id = projections.instance_domains.instance_id) AS f` + ` LEFT JOIN projections.instances ON f.id = projections.instances.id` + ` LEFT JOIN projections.instance_domains ON f.id = projections.instance_domains.instance_id` + ` AS OF SYSTEM TIME '-1 ms'` diff --git a/proto/zitadel/instance.proto b/proto/zitadel/instance.proto index 88b2d269f0..e6fdbd3411 100644 --- a/proto/zitadel/instance.proto +++ b/proto/zitadel/instance.proto @@ -71,6 +71,7 @@ message Query { option (validate.required) = true; IdsQuery id_query = 1; + DomainsQuery domain_query = 2; } } @@ -83,6 +84,17 @@ message IdsQuery { ]; } +message DomainsQuery { + repeated string domains = 1 [ + (validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_items: 20, + example: "[\"my-instace.zitadel.cloud\", \"auth.custom.com\"]"; + description: "Return the instances that have the requested domains"; + } + ]; +} + enum FieldName { FIELD_NAME_UNSPECIFIED = 0; FIELD_NAME_ID = 1; From 7b0506c19c0343079d8be19a360e34a1a735b18f Mon Sep 17 00:00:00 2001 From: Walnuts Date: Thu, 26 Oct 2023 05:18:36 +0900 Subject: [PATCH 37/48] fix(i18n): Corrected Japanese translation (#6783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: change ja 18n Co-authored-by: Tim Möhlmann --- console/src/assets/i18n/ja.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index bd5ddecb8d..6ede49ed44 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -43,10 +43,10 @@ } }, "ONBOARDING": { - "DESCRIPTION": "オンボーディングの手順", + "DESCRIPTION": "チュートリアル", "MOREDESCRIPTION": "より多くのショートカット", "COMPLETED": "完了", - "DISMISS": "いいえ、私はプロです。", + "DISMISS": "熟知しているので必要ありません。", "CARD": { "TITLE": "ZITADELの起動", "DESCRIPTION": "このチェックリストを使用して、重要な手順を確認しながらインスタンスをセットアップします。" @@ -276,8 +276,8 @@ "MINLENGTH": "{{requiredLength}} 文字以上の文字列が必要です。", "UPPERCASEMISSING": "大文字を含める必要があります。", "LOWERCASEMISSING": "小文字を含める必要があります。", - "SYMBOLERROR": "シンボルや句読点を含める必要があります。", - "NUMBERERROR": "小数点を含める必要があります。", + "SYMBOLERROR": "記号を含める必要があります。", + "NUMBERERROR": "数字を含める必要があります。", "PWNOTEQUAL": "パスワードが一致しません。", "PHONE": "電話番号は + で始まる必要があります。" }, @@ -599,7 +599,7 @@ "EDITDESC": "下のフィールドに新しい電話番号を入力してください。", "DELETETITLE": "電話番号の削除", "DELETEDESC": "本当に電話番号を削除してよろしいですか?", - "OTPSMSREMOVALWARNING": "このアカウントは、この電話番号を 2 番目の要素として使用します。続行すると使用できなくなります。" + "OTPSMSREMOVALWARNING": "このアカウントは、この電話番号を二要素認証に使用しています。続行すると二要素認証が使用できなくなります。" }, "RESENDCODE": "再送信コード", "ENTERCODE": "認証", From cb7b50b5130d0dcf49e90014b460e67c4314ff4f Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Thu, 26 Oct 2023 07:54:09 +0200 Subject: [PATCH 38/48] feat: add attribute to only enable specific themes (#6798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: enable only specific themes in label policy * feat: enable only specific themes in label policy * feat: enable only specific themes in label policy * feat: enable only specific themes in label policy * add management in console * pass enabledTheme * render login ui based on enabled theme * add in branding / settings service and name consistently * update console to latest proto state * fix console linting * fix linting * cleanup * add translations --------- Co-authored-by: Livio Spring Co-authored-by: Tim Möhlmann --- .../private-labeling-policy.component.html | 121 +++++++++++++++--- .../private-labeling-policy.component.ts | 31 ++++- .../private-labeling-policy.module.ts | 2 + console/src/assets/i18n/bg.json | 5 + console/src/assets/i18n/de.json | 5 + console/src/assets/i18n/en.json | 5 + console/src/assets/i18n/es.json | 5 + console/src/assets/i18n/fr.json | 5 + console/src/assets/i18n/it.json | 5 + console/src/assets/i18n/ja.json | 5 + console/src/assets/i18n/mk.json | 5 + console/src/assets/i18n/pl.json | 5 + console/src/assets/i18n/pt.json | 5 + console/src/assets/i18n/zh.json | 5 + .../api/grpc/admin/label_policy_converter.go | 17 +++ .../grpc/management/policy_label_converter.go | 18 +++ internal/api/grpc/policy/label_policy.go | 14 ++ .../grpc/settings/v2/settings_converter.go | 14 ++ .../settings/v2/settings_converter_test.go | 2 + internal/api/ui/login/renderer.go | 29 ++++- .../login/static/resources/scripts/theme.js | 20 ++- .../api/ui/login/static/templates/main.html | 2 +- .../eventsourcing/eventstore/auth_request.go | 1 + internal/command/instance.go | 2 + internal/command/instance_converter.go | 1 + internal/command/instance_policy_label.go | 8 +- .../command/instance_policy_label_model.go | 5 + .../command/instance_policy_label_test.go | 37 +++++- internal/command/org_policy_label.go | 6 +- internal/command/org_policy_label_model.go | 5 + internal/command/org_policy_label_test.go | 32 ++++- internal/command/policy_label_model.go | 5 + internal/domain/policy_label.go | 9 ++ internal/query/label_policy.go | 7 + internal/query/projection/label_policy.go | 10 +- .../query/projection/label_policy_test.go | 74 ++++++----- internal/repository/instance/policy_label.go | 5 +- internal/repository/org/policy_label.go | 5 +- internal/repository/policy/label.go | 55 ++++---- proto/zitadel/admin.proto | 5 + proto/zitadel/management.proto | 10 ++ proto/zitadel/policy.proto | 8 ++ .../settings/v2beta/branding_settings.proto | 12 ++ 43 files changed, 527 insertions(+), 100 deletions(-) diff --git a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html index 6ec47521a8..b649ef7869 100644 --- a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html +++ b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.html @@ -7,27 +7,11 @@
- - -
- - {{ 'POLICY.PRIVATELABELING.LIGHT' | translate }} -
-
-
- -
- - {{ 'POLICY.PRIVATELABELING.DARK' | translate }} -
-
-
-
- @@ -48,6 +32,109 @@
+
+ +
+ + +
+ + {{ 'POLICY.PRIVATELABELING.THEMEMODE.THEME_MODE_AUTO' | translate }} +
+
+
+ +
+ + {{ 'POLICY.PRIVATELABELING.THEMEMODE.THEME_MODE_LIGHT' | translate }} +
+
+
+ +
+ + {{ 'POLICY.PRIVATELABELING.THEMEMODE.THEME_MODE_DARK' | translate }} +
+
+
+
+ + + +
+ + {{ 'POLICY.PRIVATELABELING.LIGHT' | translate }} +
+
+
+ +
+ + {{ 'POLICY.PRIVATELABELING.DARK' | translate }} +
+
+
+
diff --git a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.ts b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.ts index 6e579352de..d81a1eb5ab 100644 --- a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.ts +++ b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.component.ts @@ -15,7 +15,7 @@ import { UpdateCustomLabelPolicyRequest, } from 'src/app/proto/generated/zitadel/management_pb'; import { Org } from 'src/app/proto/generated/zitadel/org_pb'; -import { LabelPolicy } from 'src/app/proto/generated/zitadel/policy_pb'; +import { LabelPolicy, ThemeMode } from 'src/app/proto/generated/zitadel/policy_pb'; import { AdminService } from 'src/app/services/admin.service'; import { AssetEndpoint, AssetService, AssetType } from 'src/app/services/asset.service'; import { ManagementService } from 'src/app/services/mgmt.service'; @@ -88,6 +88,7 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy { public View: any = View; public ColorType: any = ColorType; public AssetType: any = AssetType; + public ThemeMode: any = ThemeMode; public fontName = ''; @@ -106,6 +107,32 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy { private dialog: MatDialog, ) {} + public toggleThemeMode(): void { + if (this.view === View.CURRENT) { + return; + } + if (this.previewData?.themeMode === ThemeMode.THEME_MODE_LIGHT) { + this.theme = Theme.LIGHT; + } + if (this.previewData?.themeMode === ThemeMode.THEME_MODE_DARK) { + this.theme = Theme.DARK; + } + this.savePolicy(); + } + + public toggleView(view: View): void { + let themeMode = this.data?.themeMode; + if (view === View.PREVIEW) { + themeMode = this.previewData?.themeMode; + } + if (themeMode === ThemeMode.THEME_MODE_LIGHT) { + this.theme = Theme.LIGHT; + } + if (themeMode === ThemeMode.THEME_MODE_DARK) { + this.theme = Theme.DARK; + } + } + public toggleHoverLogo(theme: Theme, isHovering: boolean): void { if (theme === Theme.DARK) { this.isHoveringOverDarkLogo = isHovering; @@ -614,6 +641,8 @@ export class PrivateLabelingPolicyComponent implements OnInit, OnDestroy { req.setDisableWatermark(this.previewData.disableWatermark); req.setHideLoginNameSuffix(this.previewData.hideLoginNameSuffix); + + req.setThemeMode(this.previewData.themeMode); } } diff --git a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.module.ts b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.module.ts index efbd49ebde..d0f0a30bc9 100644 --- a/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.module.ts +++ b/console/src/app/modules/policies/private-labeling-policy/private-labeling-policy.module.ts @@ -9,6 +9,7 @@ import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/lega import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { ColorChromeModule } from 'ngx-color/chrome'; @@ -49,6 +50,7 @@ import { PrivateLabelingPolicyComponent } from './private-labeling-policy.compon WarnDialogModule, HasRolePipeModule, MatProgressSpinnerModule, + MatSelectModule, MatExpansionModule, InfoSectionModule, ], diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 87141b3896..5baa2c5687 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1197,6 +1197,11 @@ "ERROR": "Потребителят не може да бъде намерен!", "PRIMARYBUTTON": "следващия", "SECONDARYBUTTON": "регистрирам" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Автоматичен режим", + "THEME_MODE_LIGHT": "Само светъл режим", + "THEME_MODE_DARK": "Само тъмен режим" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 0c67a27ba3..7a2e9a72a5 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1203,6 +1203,11 @@ "ERROR": "Benutzer konnte nicht gefunden werden!", "PRIMARYBUTTON": "weiter", "SECONDARYBUTTON": "registrieren" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Automatischer Modus", + "THEME_MODE_LIGHT": "Nur heller Modus", + "THEME_MODE_DARK": "Nur dunkler Modus" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 354b272115..f0f4537c10 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1204,6 +1204,11 @@ "ERROR": "User could not be found!", "PRIMARYBUTTON": "next", "SECONDARYBUTTON": "register" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Auto Mode", + "THEME_MODE_LIGHT": "Light Mode only", + "THEME_MODE_DARK": "Dark Mode only" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 8475622457..acbeffa903 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1204,6 +1204,11 @@ "ERROR": "¡No se encontró el usuario!", "PRIMARYBUTTON": "siguiente", "SECONDARYBUTTON": "registrar" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Modo automático", + "THEME_MODE_LIGHT": "Sólo modo claro", + "THEME_MODE_DARK": "Sólo modo oscuro" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 0ec28c504d..7821e4b51a 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1203,6 +1203,11 @@ "ERROR": "L'utilisateur n'a pas pu être trouvé !", "PRIMARYBUTTON": "suivant", "SECONDARYBUTTON": "enregistrez-vous" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Mode automatique", + "THEME_MODE_LIGHT": "Mode clair uniquement", + "THEME_MODE_DARK": "Mode sombre uniquement" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 11150dc8dd..6c04858ac0 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1203,6 +1203,11 @@ "ERROR": "L'utente non \u00e8 stato trovato!", "PRIMARYBUTTON": "Avanti", "SECONDARYBUTTON": "Registra" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Modalità automatica", + "THEME_MODE_LIGHT": "Solo modalità luminosa", + "THEME_MODE_DARK": "Solo modalità oscura" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 6ede49ed44..52dbfb8163 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1200,6 +1200,11 @@ "ERROR": "ユーザーは見つかりません!", "PRIMARYBUTTON": "次へ", "SECONDARYBUTTON": "登録" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "自動モード", + "THEME_MODE_LIGHT": "ライトモードのみ", + "THEME_MODE_DARK": "ダークモードのみ" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 8ce464ddf3..fe7be90bae 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1205,6 +1205,11 @@ "ERROR": "Корисникот не е пронајден!", "PRIMARYBUTTON": "следно", "SECONDARYBUTTON": "регистрирај се" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Автоматски режим", + "THEME_MODE_LIGHT": "Само светлосен режим", + "THEME_MODE_DARK": "Само темен режим" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index c622e94bd2..03e701e20a 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1203,6 +1203,11 @@ "ERROR": "Nie znaleziono użytkownika!", "PRIMARYBUTTON": "Dalej", "SECONDARYBUTTON": "Zarejestruj" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Tryb automatyczny", + "THEME_MODE_LIGHT": "Tylko tryb jasny", + "THEME_MODE_DARK": "Tylko tryb ciemny" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 47f98ea910..add9cb30fa 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1205,6 +1205,11 @@ "ERROR": "Usuário não encontrado!", "PRIMARYBUTTON": "próximo", "SECONDARYBUTTON": "registrar" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Modo Automático", + "THEME_MODE_LIGHT": "Somente modo claro", + "THEME_MODE_DARK": "Somente modo escuro" } }, "PWD_AGE": { diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 9fac71e227..5d1bd590c0 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1202,6 +1202,11 @@ "ERROR": "找不到用户!", "PRIMARYBUTTON": "下一步", "SECONDARYBUTTON": "注册" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "自动模式", + "THEME_MODE_LIGHT": "仅限灯光模式", + "THEME_MODE_DARK": "仅限深色模式" } }, "PWD_AGE": { diff --git a/internal/api/grpc/admin/label_policy_converter.go b/internal/api/grpc/admin/label_policy_converter.go index e99ad7c0e3..e6f55124ed 100644 --- a/internal/api/grpc/admin/label_policy_converter.go +++ b/internal/api/grpc/admin/label_policy_converter.go @@ -3,6 +3,7 @@ package admin import ( "github.com/zitadel/zitadel/internal/domain" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy" ) func updateLabelPolicyToDomain(policy *admin_pb.UpdateLabelPolicyRequest) *domain.LabelPolicy { @@ -17,5 +18,21 @@ func updateLabelPolicyToDomain(policy *admin_pb.UpdateLabelPolicyRequest) *domai FontColorDark: policy.FontColorDark, HideLoginNameSuffix: policy.HideLoginNameSuffix, DisableWatermark: policy.DisableWatermark, + ThemeMode: themeModeToDomain(policy.ThemeMode), + } +} + +func themeModeToDomain(theme policy_pb.ThemeMode) domain.LabelPolicyThemeMode { + switch theme { + case policy_pb.ThemeMode_THEME_MODE_AUTO: + return domain.LabelPolicyThemeAuto + case policy_pb.ThemeMode_THEME_MODE_DARK: + return domain.LabelPolicyThemeDark + case policy_pb.ThemeMode_THEME_MODE_LIGHT: + return domain.LabelPolicyThemeLight + case policy_pb.ThemeMode_THEME_MODE_UNSPECIFIED: + return domain.LabelPolicyThemeAuto + default: + return domain.LabelPolicyThemeAuto } } diff --git a/internal/api/grpc/management/policy_label_converter.go b/internal/api/grpc/management/policy_label_converter.go index 9c399cfe33..6d79ca652a 100644 --- a/internal/api/grpc/management/policy_label_converter.go +++ b/internal/api/grpc/management/policy_label_converter.go @@ -3,6 +3,7 @@ package management import ( "github.com/zitadel/zitadel/internal/domain" mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" + policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy" ) func AddLabelPolicyToDomain(p *mgmt_pb.AddCustomLabelPolicyRequest) *domain.LabelPolicy { @@ -17,6 +18,22 @@ func AddLabelPolicyToDomain(p *mgmt_pb.AddCustomLabelPolicyRequest) *domain.Labe FontColorDark: p.FontColorDark, HideLoginNameSuffix: p.HideLoginNameSuffix, DisableWatermark: p.DisableWatermark, + ThemeMode: themeModeToDomain(p.ThemeMode), + } +} + +func themeModeToDomain(theme policy_pb.ThemeMode) domain.LabelPolicyThemeMode { + switch theme { + case policy_pb.ThemeMode_THEME_MODE_AUTO: + return domain.LabelPolicyThemeAuto + case policy_pb.ThemeMode_THEME_MODE_DARK: + return domain.LabelPolicyThemeDark + case policy_pb.ThemeMode_THEME_MODE_LIGHT: + return domain.LabelPolicyThemeLight + case policy_pb.ThemeMode_THEME_MODE_UNSPECIFIED: + return domain.LabelPolicyThemeAuto + default: + return domain.LabelPolicyThemeAuto } } @@ -32,5 +49,6 @@ func updateLabelPolicyToDomain(p *mgmt_pb.UpdateCustomLabelPolicyRequest) *domai FontColorDark: p.FontColorDark, HideLoginNameSuffix: p.HideLoginNameSuffix, DisableWatermark: p.DisableWatermark, + ThemeMode: themeModeToDomain(p.ThemeMode), } } diff --git a/internal/api/grpc/policy/label_policy.go b/internal/api/grpc/policy/label_policy.go index 0b925c3720..1fade4c914 100644 --- a/internal/api/grpc/policy/label_policy.go +++ b/internal/api/grpc/policy/label_policy.go @@ -26,6 +26,7 @@ func ModelLabelPolicyToPb(policy *query.LabelPolicy, assetPrefix string) *policy DisableWatermark: policy.WatermarkDisabled, HideLoginNameSuffix: policy.HideLoginNameSuffix, + ThemeMode: themeModeToPb(policy.ThemeMode), Details: object.ToViewDetailsPb( policy.Sequence, policy.CreationDate, @@ -34,3 +35,16 @@ func ModelLabelPolicyToPb(policy *query.LabelPolicy, assetPrefix string) *policy ), } } + +func themeModeToPb(theme domain.LabelPolicyThemeMode) policy_pb.ThemeMode { + switch theme { + case domain.LabelPolicyThemeAuto: + return policy_pb.ThemeMode_THEME_MODE_AUTO + case domain.LabelPolicyThemeDark: + return policy_pb.ThemeMode_THEME_MODE_DARK + case domain.LabelPolicyThemeLight: + return policy_pb.ThemeMode_THEME_MODE_LIGHT + default: + return policy_pb.ThemeMode_THEME_MODE_AUTO + } +} diff --git a/internal/api/grpc/settings/v2/settings_converter.go b/internal/api/grpc/settings/v2/settings_converter.go index 7c28776299..69a494d027 100644 --- a/internal/api/grpc/settings/v2/settings_converter.go +++ b/internal/api/grpc/settings/v2/settings_converter.go @@ -107,6 +107,20 @@ func brandingSettingsToPb(current *query.LabelPolicy, assetPrefix string) *setti DisableWatermark: current.WatermarkDisabled, HideLoginNameSuffix: current.HideLoginNameSuffix, ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), + ThemeMode: themeModeToPb(current.ThemeMode), + } +} + +func themeModeToPb(themeMode domain.LabelPolicyThemeMode) settings.ThemeMode { + switch themeMode { + case domain.LabelPolicyThemeAuto: + return settings.ThemeMode_THEME_MODE_AUTO + case domain.LabelPolicyThemeLight: + return settings.ThemeMode_THEME_MODE_LIGHT + case domain.LabelPolicyThemeDark: + return settings.ThemeMode_THEME_MODE_DARK + default: + return settings.ThemeMode_THEME_MODE_AUTO } } diff --git a/internal/api/grpc/settings/v2/settings_converter_test.go b/internal/api/grpc/settings/v2/settings_converter_test.go index 133715d8b1..6e441a33ba 100644 --- a/internal/api/grpc/settings/v2/settings_converter_test.go +++ b/internal/api/grpc/settings/v2/settings_converter_test.go @@ -258,6 +258,7 @@ func Test_brandingSettingsToPb(t *testing.T) { FontURL: "fonts", WatermarkDisabled: true, HideLoginNameSuffix: true, + ThemeMode: domain.LabelPolicyThemeDark, IsDefault: true, } want := &settings.BrandingSettings{ @@ -281,6 +282,7 @@ func Test_brandingSettingsToPb(t *testing.T) { DisableWatermark: true, HideLoginNameSuffix: true, ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, + ThemeMode: settings.ThemeMode_THEME_MODE_DARK, } got := brandingSettingsToPb(arg, "http://example.com") diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index 85d54cd015..d81ab4567e 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -381,8 +381,6 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI Title: title, Description: description, Theme: l.getTheme(r), - ThemeMode: l.getThemeMode(r), - DarkMode: l.isDarkMode(r), PrivateLabelingOrgID: l.getPrivateLabelingID(r, authReq), OrgID: l.getOrgID(r, authReq), OrgName: l.getOrgName(authReq), @@ -412,6 +410,9 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, titleI } privacyPolicy = policy.ToDomain() } + baseData.ThemeMode = l.getThemeMode(baseData.LabelPolicy) + baseData.ThemeClass = l.getThemeClass(r, baseData.LabelPolicy) + baseData.DarkMode = l.isDarkMode(r, baseData.LabelPolicy) baseData = l.setLinksOnBaseData(baseData, privacyPolicy) return baseData } @@ -480,14 +481,22 @@ func (l *Login) getTheme(r *http.Request) string { return "zitadel" } -func (l *Login) getThemeMode(r *http.Request) string { - if l.isDarkMode(r) { +// getThemeClass returns the css class for the login html. +// Possible values are `lgn-light-theme` and `lgn-dark-theme` and are based on the policy first +// and if it's set to auto the cookie is checked. +func (l *Login) getThemeClass(r *http.Request, policy *domain.LabelPolicy) string { + if l.isDarkMode(r, policy) { return "lgn-dark-theme" } return "lgn-light-theme" } -func (l *Login) isDarkMode(r *http.Request) bool { +// isDarkMode checks policy first and if not set to specifically use dark or light only, +// it will also check the cookie. +func (l *Login) isDarkMode(r *http.Request, policy *domain.LabelPolicy) bool { + if mode := l.getThemeMode(policy); mode != domain.LabelPolicyThemeAuto { + return mode == domain.LabelPolicyThemeDark + } cookie, err := r.Cookie("mode") if err != nil { return false @@ -495,6 +504,13 @@ func (l *Login) isDarkMode(r *http.Request) bool { return strings.HasSuffix(cookie.Value, "dark") } +func (l *Login) getThemeMode(policy *domain.LabelPolicy) domain.LabelPolicyThemeMode { + if policy != nil { + return policy.ThemeMode + } + return domain.LabelPolicyThemeAuto +} + func (l *Login) getOrgID(r *http.Request, authReq *domain.AuthRequest) string { if authReq == nil { return r.FormValue(queryOrgID) @@ -607,7 +623,8 @@ type baseData struct { Title string Description string Theme string - ThemeMode string + ThemeMode domain.LabelPolicyThemeMode + ThemeClass string DarkMode bool PrivateLabelingOrgID string OrgID string diff --git a/internal/api/ui/login/static/resources/scripts/theme.js b/internal/api/ui/login/static/resources/scripts/theme.js index c5b088f6ba..dce18a72b6 100644 --- a/internal/api/ui/login/static/resources/scripts/theme.js +++ b/internal/api/ui/login/static/resources/scripts/theme.js @@ -1,10 +1,16 @@ -const usesDarkTheme = hasDarkModeOverwriteCookie() || (!hasLightModeOverwriteCookie() && window.matchMedia('(prefers-color-scheme: dark)').matches); -if (usesDarkTheme) { - document.documentElement.classList.replace('lgn-light-theme', 'lgn-dark-theme'); - writeModeCookie('dark'); -} else { - document.documentElement.classList.replace('lgn-dark-theme', 'lgn-light-theme'); - writeModeCookie('light'); +if (isAutoMode()) { + const usesDarkTheme = hasDarkModeOverwriteCookie() || (!hasLightModeOverwriteCookie() && window.matchMedia('(prefers-color-scheme: dark)').matches); + if (usesDarkTheme) { + document.documentElement.classList.replace('lgn-light-theme', 'lgn-dark-theme'); + writeModeCookie('dark'); + } else { + document.documentElement.classList.replace('lgn-dark-theme', 'lgn-light-theme'); + writeModeCookie('light'); + } +} + +function isAutoMode() { + return document.documentElement.dataset["themeMode"] === "0" } function hasDarkModeOverwriteCookie() { diff --git a/internal/api/ui/login/static/templates/main.html b/internal/api/ui/login/static/templates/main.html index 04ec281ebe..f4ef0943fe 100644 --- a/internal/api/ui/login/static/templates/main.html +++ b/internal/api/ui/login/static/templates/main.html @@ -1,6 +1,6 @@ {{define "main-top"}} - + diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 24ef2f15f9..6e40aff730 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -1335,6 +1335,7 @@ func labelPolicyToDomain(p *query.LabelPolicy) *domain.LabelPolicy { HideLoginNameSuffix: p.HideLoginNameSuffix, ErrorMsgPopup: p.ShouldErrorPopup, DisableWatermark: p.WatermarkDisabled, + ThemeMode: p.ThemeMode, } } diff --git a/internal/command/instance.go b/internal/command/instance.go index 4c5bb2faa5..df3420198c 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -97,6 +97,7 @@ type InstanceSetup struct { HideLoginNameSuffix bool ErrorMsgPopup bool DisableWatermark bool + ThemeMode domain.LabelPolicyThemeMode } LockoutPolicy struct { MaxAttempts uint64 @@ -276,6 +277,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str setup.LabelPolicy.HideLoginNameSuffix, setup.LabelPolicy.ErrorMsgPopup, setup.LabelPolicy.DisableWatermark, + setup.LabelPolicy.ThemeMode, ), prepareActivateDefaultLabelPolicy(instanceAgg), diff --git a/internal/command/instance_converter.go b/internal/command/instance_converter.go index 6d7f605751..2e44fc7a7f 100644 --- a/internal/command/instance_converter.go +++ b/internal/command/instance_converter.go @@ -59,6 +59,7 @@ func writeModelToLabelPolicy(wm *LabelPolicyWriteModel) *domain.LabelPolicy { HideLoginNameSuffix: wm.HideLoginNameSuffix, ErrorMsgPopup: wm.ErrorMsgPopup, DisableWatermark: wm.DisableWatermark, + ThemeMode: wm.ThemeMode, } } diff --git a/internal/command/instance_policy_label.go b/internal/command/instance_policy_label.go index 19bd63004d..4a083fce27 100644 --- a/internal/command/instance_policy_label.go +++ b/internal/command/instance_policy_label.go @@ -15,7 +15,7 @@ import ( func (c *Commands) AddDefaultLabelPolicy( ctx context.Context, primaryColor, backgroundColor, warnColor, fontColor, primaryColorDark, backgroundColorDark, warnColorDark, fontColorDark string, - hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, + hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, themeMode domain.LabelPolicyThemeMode, ) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, @@ -32,6 +32,7 @@ func (c *Commands) AddDefaultLabelPolicy( hideLoginNameSuffix, errorMsgPopup, disableWatermark, + themeMode, )) if err != nil { return nil, err @@ -69,7 +70,8 @@ func (c *Commands) ChangeDefaultLabelPolicy(ctx context.Context, policy *domain. policy.FontColorDark, policy.HideLoginNameSuffix, policy.ErrorMsgPopup, - policy.DisableWatermark) + policy.DisableWatermark, + policy.ThemeMode) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-28fHe", "Errors.IAM.LabelPolicy.NotChanged") } @@ -384,6 +386,7 @@ func prepareAddDefaultLabelPolicy( hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, + themeMode domain.LabelPolicyThemeMode, ) preparation.Validation { return func() (preparation.CreateCommands, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { @@ -412,6 +415,7 @@ func prepareAddDefaultLabelPolicy( hideLoginNameSuffix, errorMsgPopup, disableWatermark, + themeMode, ), }, nil }, nil diff --git a/internal/command/instance_policy_label_model.go b/internal/command/instance_policy_label_model.go index 9dad068026..2b32ebe7cb 100644 --- a/internal/command/instance_policy_label_model.go +++ b/internal/command/instance_policy_label_model.go @@ -4,6 +4,7 @@ import ( "context" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/policy" @@ -97,6 +98,7 @@ func (wm *InstanceLabelPolicyWriteModel) NewChangedEvent( hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, + themeMode domain.LabelPolicyThemeMode, ) (*instance.LabelPolicyChangedEvent, bool) { changes := make([]policy.LabelPolicyChanges, 0) if wm.PrimaryColor != primaryColor { @@ -132,6 +134,9 @@ func (wm *InstanceLabelPolicyWriteModel) NewChangedEvent( if wm.DisableWatermark != disableWatermark { changes = append(changes, policy.ChangeDisableWatermark(disableWatermark)) } + if wm.ThemeMode != themeMode { + changes = append(changes, policy.ChangeThemeMode(themeMode)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/instance_policy_label_test.go b/internal/command/instance_policy_label_test.go index 5b7b9dcbc5..908186a367 100644 --- a/internal/command/instance_policy_label_test.go +++ b/internal/command/instance_policy_label_test.go @@ -36,6 +36,7 @@ func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { hideLoginNameSuffix bool errorMsgPopup bool disableWatermark bool + themeMode domain.LabelPolicyThemeMode } type res struct { want *domain.ObjectDetails @@ -67,6 +68,7 @@ func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -85,6 +87,7 @@ func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { hideLoginNameSuffix: true, errorMsgPopup: true, disableWatermark: true, + themeMode: domain.LabelPolicyThemeAuto, }, res: res{ err: caos_errs.IsErrorAlreadyExists, @@ -110,6 +113,7 @@ func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeDark, ), ), ), @@ -127,6 +131,7 @@ func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { hideLoginNameSuffix: true, errorMsgPopup: true, disableWatermark: true, + themeMode: domain.LabelPolicyThemeDark, }, res: res{ want: &domain.ObjectDetails{ @@ -153,6 +158,7 @@ func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { tt.args.hideLoginNameSuffix, tt.args.errorMsgPopup, tt.args.disableWatermark, + tt.args.themeMode, ) if tt.res.err == nil { assert.NoError(t, err) @@ -225,6 +231,7 @@ func TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -244,6 +251,7 @@ func TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) { HideLoginNameSuffix: true, ErrorMsgPopup: true, DisableWatermark: true, + ThemeMode: domain.LabelPolicyThemeAuto, }, }, res: res{ @@ -270,6 +278,7 @@ func TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -286,7 +295,8 @@ func TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) { "#000000", false, false, - false), + false, + domain.LabelPolicyThemeDark), ), ), }, @@ -304,6 +314,7 @@ func TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) { HideLoginNameSuffix: false, ErrorMsgPopup: false, DisableWatermark: false, + ThemeMode: domain.LabelPolicyThemeDark, }, }, res: res{ @@ -324,6 +335,7 @@ func TestCommandSide_ChangeDefaultLabelPolicy(t *testing.T) { HideLoginNameSuffix: false, ErrorMsgPopup: false, DisableWatermark: false, + ThemeMode: domain.LabelPolicyThemeDark, }, }, }, @@ -399,6 +411,7 @@ func TestCommandSide_ActivateDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -500,6 +513,7 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -541,6 +555,7 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -645,6 +660,7 @@ func TestCommandSide_RemoveLogoDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -684,6 +700,7 @@ func TestCommandSide_RemoveLogoDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -793,6 +810,7 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -834,6 +852,7 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -938,6 +957,7 @@ func TestCommandSide_RemoveIconDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1049,6 +1069,7 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1091,6 +1112,7 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1195,6 +1217,7 @@ func TestCommandSide_RemoveLogoDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1234,6 +1257,7 @@ func TestCommandSide_RemoveLogoDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1343,6 +1367,7 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1384,6 +1409,7 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1488,6 +1514,7 @@ func TestCommandSide_RemoveIconDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1527,6 +1554,7 @@ func TestCommandSide_RemoveIconDarkDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1636,6 +1664,7 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1677,6 +1706,7 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1781,6 +1811,7 @@ func TestCommandSide_RemoveFontDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1820,6 +1851,7 @@ func TestCommandSide_RemoveFontDefaultLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1867,7 +1899,7 @@ func TestCommandSide_RemoveFontDefaultLabelPolicy(t *testing.T) { } } -func newDefaultLabelPolicyChangedEvent(ctx context.Context, primaryColor, backgroundColor, warnColor, fontColor, primaryColorDark, backgroundColorDark, warnColorDark, fontColorDark string, hideLoginNameSuffix, errMsgPopup, disableWatermark bool) *instance.LabelPolicyChangedEvent { +func newDefaultLabelPolicyChangedEvent(ctx context.Context, primaryColor, backgroundColor, warnColor, fontColor, primaryColorDark, backgroundColorDark, warnColorDark, fontColorDark string, hideLoginNameSuffix, errMsgPopup, disableWatermark bool, theme domain.LabelPolicyThemeMode) *instance.LabelPolicyChangedEvent { event, _ := instance.NewLabelPolicyChangedEvent(ctx, &instance.NewAggregate("INSTANCE").Aggregate, []policy.LabelPolicyChanges{ @@ -1882,6 +1914,7 @@ func newDefaultLabelPolicyChangedEvent(ctx context.Context, primaryColor, backgr policy.ChangeHideLoginNameSuffix(hideLoginNameSuffix), policy.ChangeErrorMsgPopup(errMsgPopup), policy.ChangeDisableWatermark(disableWatermark), + policy.ChangeThemeMode(theme), }, ) return event diff --git a/internal/command/org_policy_label.go b/internal/command/org_policy_label.go index 8d4535c528..6698baff61 100644 --- a/internal/command/org_policy_label.go +++ b/internal/command/org_policy_label.go @@ -39,7 +39,8 @@ func (c *Commands) AddLabelPolicy(ctx context.Context, resourceOwner string, pol policy.FontColorDark, policy.HideLoginNameSuffix, policy.ErrorMsgPopup, - policy.DisableWatermark)) + policy.DisableWatermark, + policy.ThemeMode)) if err != nil { return nil, err } @@ -80,7 +81,8 @@ func (c *Commands) ChangeLabelPolicy(ctx context.Context, resourceOwner string, policy.FontColorDark, policy.HideLoginNameSuffix, policy.ErrorMsgPopup, - policy.DisableWatermark) + policy.DisableWatermark, + policy.ThemeMode) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-8nfSr", "Errors.Org.LabelPolicy.NotChanged") } diff --git a/internal/command/org_policy_label_model.go b/internal/command/org_policy_label_model.go index 83cd39d25a..d8e510f425 100644 --- a/internal/command/org_policy_label_model.go +++ b/internal/command/org_policy_label_model.go @@ -3,6 +3,7 @@ package command import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" @@ -98,6 +99,7 @@ func (wm *OrgLabelPolicyWriteModel) NewChangedEvent( hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, + themeMode domain.LabelPolicyThemeMode, ) (*org.LabelPolicyChangedEvent, bool) { changes := make([]policy.LabelPolicyChanges, 0) if wm.PrimaryColor != primaryColor { @@ -133,6 +135,9 @@ func (wm *OrgLabelPolicyWriteModel) NewChangedEvent( if wm.DisableWatermark != disableWatermark { changes = append(changes, policy.ChangeDisableWatermark(disableWatermark)) } + if wm.ThemeMode != themeMode { + changes = append(changes, policy.ChangeThemeMode(themeMode)) + } if len(changes) == 0 { return nil, false } diff --git a/internal/command/org_policy_label_test.go b/internal/command/org_policy_label_test.go index d317b8cd81..3cd3ea81d5 100644 --- a/internal/command/org_policy_label_test.go +++ b/internal/command/org_policy_label_test.go @@ -75,6 +75,7 @@ func TestCommandSide_AddLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -121,6 +122,7 @@ func TestCommandSide_AddLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeDark, ), ), ), @@ -140,6 +142,7 @@ func TestCommandSide_AddLabelPolicy(t *testing.T) { HideLoginNameSuffix: true, ErrorMsgPopup: true, DisableWatermark: true, + ThemeMode: domain.LabelPolicyThemeDark, }, }, res: res{ @@ -159,6 +162,7 @@ func TestCommandSide_AddLabelPolicy(t *testing.T) { HideLoginNameSuffix: true, ErrorMsgPopup: true, DisableWatermark: true, + ThemeMode: domain.LabelPolicyThemeDark, }, }, }, @@ -260,6 +264,7 @@ func TestCommandSide_ChangeLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -280,6 +285,7 @@ func TestCommandSide_ChangeLabelPolicy(t *testing.T) { HideLoginNameSuffix: true, ErrorMsgPopup: true, DisableWatermark: true, + ThemeMode: domain.LabelPolicyThemeAuto, }, }, res: res{ @@ -306,6 +312,7 @@ func TestCommandSide_ChangeLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -323,7 +330,8 @@ func TestCommandSide_ChangeLabelPolicy(t *testing.T) { "#000000", false, false, - false), + false, + domain.LabelPolicyThemeDark), ), ), }, @@ -342,6 +350,7 @@ func TestCommandSide_ChangeLabelPolicy(t *testing.T) { HideLoginNameSuffix: false, ErrorMsgPopup: false, DisableWatermark: false, + ThemeMode: domain.LabelPolicyThemeDark, }, }, res: res{ @@ -361,6 +370,7 @@ func TestCommandSide_ChangeLabelPolicy(t *testing.T) { HideLoginNameSuffix: false, ErrorMsgPopup: false, DisableWatermark: false, + ThemeMode: domain.LabelPolicyThemeDark, }, }, }, @@ -451,6 +461,7 @@ func TestCommandSide_ActivateLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -551,6 +562,7 @@ func TestCommandSide_RemoveLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -672,6 +684,7 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -714,6 +727,7 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -838,6 +852,7 @@ func TestCommandSide_RemoveLogoLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -974,6 +989,7 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1016,6 +1032,7 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1138,6 +1155,7 @@ func TestCommandSide_RemoveIconLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1273,6 +1291,7 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1315,6 +1334,7 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1439,6 +1459,7 @@ func TestCommandSide_RemoveLogoDarkLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1575,6 +1596,7 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1617,6 +1639,7 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1737,6 +1760,7 @@ func TestCommandSide_RemoveIconDarkLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -1864,6 +1888,7 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -1906,6 +1931,7 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), ), @@ -2026,6 +2052,7 @@ func TestCommandSide_RemoveFontLabelPolicy(t *testing.T) { true, true, true, + domain.LabelPolicyThemeAuto, ), ), eventFromEventPusher( @@ -2074,7 +2101,7 @@ func TestCommandSide_RemoveFontLabelPolicy(t *testing.T) { } } -func newLabelPolicyChangedEvent(ctx context.Context, orgID, primaryColor, backgroundColor, warnColor, fontColor, primaryColorDark, backgroundColorDark, warnColorDark, fontColorDark string, hideLoginNameSuffix, errMsgPopup, disableWatermark bool) *org.LabelPolicyChangedEvent { +func newLabelPolicyChangedEvent(ctx context.Context, orgID, primaryColor, backgroundColor, warnColor, fontColor, primaryColorDark, backgroundColorDark, warnColorDark, fontColorDark string, hideLoginNameSuffix, errMsgPopup, disableWatermark bool, theme domain.LabelPolicyThemeMode) *org.LabelPolicyChangedEvent { event, _ := org.NewLabelPolicyChangedEvent(ctx, &org.NewAggregate(orgID).Aggregate, []policy.LabelPolicyChanges{ @@ -2089,6 +2116,7 @@ func newLabelPolicyChangedEvent(ctx context.Context, orgID, primaryColor, backgr policy.ChangeHideLoginNameSuffix(hideLoginNameSuffix), policy.ChangeErrorMsgPopup(errMsgPopup), policy.ChangeDisableWatermark(disableWatermark), + policy.ChangeThemeMode(theme), }, ) return event diff --git a/internal/command/policy_label_model.go b/internal/command/policy_label_model.go index a44d0e1b56..f620b2aa61 100644 --- a/internal/command/policy_label_model.go +++ b/internal/command/policy_label_model.go @@ -28,6 +28,7 @@ type LabelPolicyWriteModel struct { HideLoginNameSuffix bool ErrorMsgPopup bool DisableWatermark bool + ThemeMode domain.LabelPolicyThemeMode State domain.PolicyState } @@ -47,6 +48,7 @@ func (wm *LabelPolicyWriteModel) Reduce() error { wm.HideLoginNameSuffix = e.HideLoginNameSuffix wm.ErrorMsgPopup = e.ErrorMsgPopup wm.DisableWatermark = e.DisableWatermark + wm.ThemeMode = e.ThemeMode wm.State = domain.PolicyStateActive case *policy.LabelPolicyChangedEvent: if e.PrimaryColor != nil { @@ -82,6 +84,9 @@ func (wm *LabelPolicyWriteModel) Reduce() error { if e.DisableWatermark != nil { wm.DisableWatermark = *e.DisableWatermark } + if e.ThemeMode != nil { + wm.ThemeMode = *e.ThemeMode + } case *policy.LabelPolicyLogoAddedEvent: wm.LogoKey = e.StoreKey case *policy.LabelPolicyLogoRemovedEvent: diff --git a/internal/domain/policy_label.go b/internal/domain/policy_label.go index f3d660a91d..b5517bc54c 100644 --- a/internal/domain/policy_label.go +++ b/internal/domain/policy_label.go @@ -34,6 +34,7 @@ type LabelPolicy struct { HideLoginNameSuffix bool ErrorMsgPopup bool DisableWatermark bool + ThemeMode LabelPolicyThemeMode } type LabelPolicyState int32 @@ -47,6 +48,14 @@ const ( labelPolicyStateCount ) +type LabelPolicyThemeMode int32 + +const ( + LabelPolicyThemeAuto LabelPolicyThemeMode = iota + LabelPolicyThemeLight + LabelPolicyThemeDark +) + func (f LabelPolicy) IsValid() error { if !colorRegex.MatchString(f.PrimaryColor) { return caos_errs.ThrowInvalidArgument(nil, "POLICY-391dG", "Errors.Policy.Label.Invalid.PrimaryColor") diff --git a/internal/query/label_policy.go b/internal/query/label_policy.go index 476c8de595..fe21987adb 100644 --- a/internal/query/label_policy.go +++ b/internal/query/label_policy.go @@ -28,6 +28,7 @@ type LabelPolicy struct { FontURL string WatermarkDisabled bool ShouldErrorPopup bool + ThemeMode domain.LabelPolicyThemeMode Dark Theme Light Theme @@ -234,6 +235,9 @@ var ( LabelPolicyOwnerRemoved = Column{ name: projection.LabelPolicyOwnerRemovedCol, } + LabelPolicyThemeMode = Column{ + name: projection.LabelPolicyThemeModeCol, + } ) func prepareLabelPolicyQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*LabelPolicy, error)) { @@ -250,6 +254,7 @@ func prepareLabelPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select LabelPolicyColFontURL.identifier(), LabelPolicyColWatermarkDisabled.identifier(), LabelPolicyColShouldErrorPopup.identifier(), + LabelPolicyThemeMode.identifier(), LabelPolicyColLightPrimaryColor.identifier(), LabelPolicyColLightWarnColor.identifier(), @@ -299,6 +304,7 @@ func prepareLabelPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Select &fontURL, &policy.WatermarkDisabled, &policy.ShouldErrorPopup, + &policy.ThemeMode, &lightPrimaryColor, &lightWarnColor, @@ -358,5 +364,6 @@ func (p *LabelPolicy) ToDomain() *domain.LabelPolicy { HideLoginNameSuffix: p.HideLoginNameSuffix, ErrorMsgPopup: p.ShouldErrorPopup, DisableWatermark: p.WatermarkDisabled, + ThemeMode: p.ThemeMode, } } diff --git a/internal/query/projection/label_policy.go b/internal/query/projection/label_policy.go index f8f6b03d5d..49f69738e4 100644 --- a/internal/query/projection/label_policy.go +++ b/internal/query/projection/label_policy.go @@ -14,7 +14,7 @@ import ( ) const ( - LabelPolicyTable = "projections.label_policies2" + LabelPolicyTable = "projections.label_policies3" LabelPolicyIDCol = "id" LabelPolicyCreationDateCol = "creation_date" @@ -29,6 +29,7 @@ const ( LabelPolicyShouldErrorPopupCol = "should_error_popup" LabelPolicyFontURLCol = "font_url" LabelPolicyOwnerRemovedCol = "owner_removed" + LabelPolicyThemeModeCol = "theme_mode" LabelPolicyLightPrimaryColorCol = "light_primary_color" LabelPolicyLightWarnColorCol = "light_warn_color" @@ -83,6 +84,7 @@ func (*labelPolicyProjection) Init() *old_handler.Check { handler.NewColumn(LabelPolicyDarkLogoURLCol, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(LabelPolicyDarkIconURLCol, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(LabelPolicyOwnerRemovedCol, handler.ColumnTypeBool, handler.Default(false)), + handler.NewColumn(LabelPolicyThemeModeCol, handler.ColumnTypeEnum, handler.Default(0)), }, handler.NewPrimaryKey(LabelPolicyInstanceIDCol, LabelPolicyIDCol, LabelPolicyStateCol), handler.WithIndex(handler.NewIndex("owner_removed", []string{LabelPolicyOwnerRemovedCol})), @@ -264,6 +266,7 @@ func (p *labelPolicyProjection) reduceAdded(event eventstore.Event) (*handler.St handler.NewCol(LabelPolicyHideLoginNameSuffixCol, policyEvent.HideLoginNameSuffix), handler.NewCol(LabelPolicyShouldErrorPopupCol, policyEvent.ErrorMsgPopup), handler.NewCol(LabelPolicyWatermarkDisabledCol, policyEvent.DisableWatermark), + handler.NewCol(LabelPolicyThemeModeCol, policyEvent.ThemeMode), }), nil } @@ -314,6 +317,9 @@ func (p *labelPolicyProjection) reduceChanged(event eventstore.Event) (*handler. if policyEvent.DisableWatermark != nil { cols = append(cols, handler.NewCol(LabelPolicyWatermarkDisabledCol, *policyEvent.DisableWatermark)) } + if policyEvent.ThemeMode != nil { + cols = append(cols, handler.NewCol(LabelPolicyThemeModeCol, *policyEvent.ThemeMode)) + } return handler.NewUpdateStatement( &policyEvent, cols, @@ -376,6 +382,7 @@ func (p *labelPolicyProjection) reduceActivated(event eventstore.Event) (*handle handler.NewCol(LabelPolicyDarkFontColorCol, nil), handler.NewCol(LabelPolicyDarkLogoURLCol, nil), handler.NewCol(LabelPolicyDarkIconURLCol, nil), + handler.NewCol(LabelPolicyThemeModeCol, nil), }, []handler.Column{ handler.NewCol(LabelPolicyChangeDateCol, nil), @@ -402,6 +409,7 @@ func (p *labelPolicyProjection) reduceActivated(event eventstore.Event) (*handle handler.NewCol(LabelPolicyDarkFontColorCol, nil), handler.NewCol(LabelPolicyDarkLogoURLCol, nil), handler.NewCol(LabelPolicyDarkIconURLCol, nil), + handler.NewCol(LabelPolicyThemeModeCol, nil), }, []handler.NamespacedCondition{ handler.NewNamespacedCondition(LabelPolicyIDCol, event.Aggregate().ID), diff --git a/internal/query/projection/label_policy_test.go b/internal/query/projection/label_policy_test.go index 553b67fc38..202e8cd32c 100644 --- a/internal/query/projection/label_policy_test.go +++ b/internal/query/projection/label_policy_test.go @@ -28,7 +28,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { testEvent( org.LabelPolicyAddedEventType, org.AggregateType, - []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b"}`), + []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b", "themeMode": 1}`), ), org.LabelPolicyAddedEventMapper), }, reduce: (&labelPolicyProjection{}).reduceAdded, @@ -38,7 +38,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.label_policies2 (creation_date, change_date, sequence, id, state, is_default, resource_owner, instance_id, light_primary_color, light_background_color, light_warn_color, light_font_color, dark_primary_color, dark_background_color, dark_warn_color, dark_font_color, hide_login_name_suffix, should_error_popup, watermark_disabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", + expectedStmt: "INSERT INTO projections.label_policies3 (creation_date, change_date, sequence, id, state, is_default, resource_owner, instance_id, light_primary_color, light_background_color, light_warn_color, light_font_color, dark_primary_color, dark_background_color, dark_warn_color, dark_font_color, hide_login_name_suffix, should_error_popup, watermark_disabled, theme_mode) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -59,6 +59,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { false, false, false, + domain.LabelPolicyThemeLight, }, }, }, @@ -72,7 +73,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { testEvent( org.LabelPolicyChangedEventType, org.AggregateType, - []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b"}`), + []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b", "themeMode": 1}`), ), org.LabelPolicyChangedEventMapper), }, reduce: (&labelPolicyProjection{}).reduceChanged, @@ -82,7 +83,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_primary_color, light_background_color, light_warn_color, light_font_color) = ($1, $2, $3, $4, $5, $6) WHERE (id = $7) AND (state = $8) AND (instance_id = $9)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_primary_color, light_background_color, light_warn_color, light_font_color, theme_mode) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (state = $9) AND (instance_id = $10)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -90,6 +91,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { "#141735", "#ff3b5b", "#ffffff", + domain.LabelPolicyThemeLight, "agg-id", domain.LabelPolicyStatePreview, "instance-id", @@ -116,7 +118,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.label_policies2 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.label_policies3 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "agg-id", "instance-id", @@ -143,7 +145,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.label_policies2 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.label_policies3 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -169,7 +171,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.label_policies2 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url FROM projections.label_policies2 AS copy_table WHERE (copy_table.id = $4) AND (copy_table.state = $5) AND (copy_table.instance_id = $6) ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url)", + expectedStmt: "INSERT INTO projections.label_policies3 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url, theme_mode) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url, theme_mode FROM projections.label_policies3 AS copy_table WHERE (copy_table.id = $4) AND (copy_table.state = $5) AND (copy_table.instance_id = $6) ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url, theme_mode) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url, EXCLUDED.theme_mode)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -200,7 +202,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -231,7 +233,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -262,7 +264,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -293,7 +295,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -324,7 +326,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -355,7 +357,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -386,7 +388,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -417,7 +419,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -448,7 +450,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -479,7 +481,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -510,7 +512,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_logo_url, light_icon_url, dark_logo_url, dark_icon_url, font_url) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (state = $9) AND (instance_id = $10)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_logo_url, light_icon_url, dark_logo_url, dark_icon_url, font_url) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (state = $9) AND (instance_id = $10)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -535,7 +537,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { testEvent( instance.LabelPolicyAddedEventType, instance.AggregateType, - []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b"}`), + []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b", "themeMode": 1}`), ), instance.LabelPolicyAddedEventMapper), }, reduce: (&labelPolicyProjection{}).reduceAdded, @@ -545,7 +547,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.label_policies2 (creation_date, change_date, sequence, id, state, is_default, resource_owner, instance_id, light_primary_color, light_background_color, light_warn_color, light_font_color, dark_primary_color, dark_background_color, dark_warn_color, dark_font_color, hide_login_name_suffix, should_error_popup, watermark_disabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", + expectedStmt: "INSERT INTO projections.label_policies3 (creation_date, change_date, sequence, id, state, is_default, resource_owner, instance_id, light_primary_color, light_background_color, light_warn_color, light_font_color, dark_primary_color, dark_background_color, dark_warn_color, dark_font_color, hide_login_name_suffix, should_error_popup, watermark_disabled, theme_mode) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -566,6 +568,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { false, false, false, + domain.LabelPolicyThemeLight, }, }, }, @@ -579,7 +582,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { testEvent( instance.LabelPolicyChangedEventType, instance.AggregateType, - []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b", "primaryColorDark": "#ffffff","backgroundColorDark": "#ffffff", "warnColorDark": "#ffffff", "fontColorDark": "#ffffff", "hideLoginNameSuffix": true, "errorMsgPopup": true, "disableWatermark": true}`), + []byte(`{"backgroundColor": "#141735", "fontColor": "#ffffff", "primaryColor": "#5282c1", "warnColor": "#ff3b5b", "primaryColorDark": "#ffffff","backgroundColorDark": "#ffffff", "warnColorDark": "#ffffff", "fontColorDark": "#ffffff", "hideLoginNameSuffix": true, "errorMsgPopup": true, "disableWatermark": true, "themeMode": 1}`), ), instance.LabelPolicyChangedEventMapper), }, reduce: (&labelPolicyProjection{}).reduceChanged, @@ -589,7 +592,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_primary_color, light_background_color, light_warn_color, light_font_color, dark_primary_color, dark_background_color, dark_warn_color, dark_font_color, hide_login_name_suffix, should_error_popup, watermark_disabled) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) WHERE (id = $14) AND (state = $15) AND (instance_id = $16)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_primary_color, light_background_color, light_warn_color, light_font_color, dark_primary_color, dark_background_color, dark_warn_color, dark_font_color, hide_login_name_suffix, should_error_popup, watermark_disabled, theme_mode) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) WHERE (id = $15) AND (state = $16) AND (instance_id = $17)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -604,6 +607,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { true, true, true, + domain.LabelPolicyThemeLight, "agg-id", domain.LabelPolicyStatePreview, "instance-id", @@ -630,7 +634,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.label_policies2 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url FROM projections.label_policies2 AS copy_table WHERE (copy_table.id = $4) AND (copy_table.state = $5) AND (copy_table.instance_id = $6) ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url)", + expectedStmt: "INSERT INTO projections.label_policies3 (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url, theme_mode) SELECT $1, $2, $3, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url, theme_mode FROM projections.label_policies3 AS copy_table WHERE (copy_table.id = $4) AND (copy_table.state = $5) AND (copy_table.instance_id = $6) ON CONFLICT (instance_id, id, state) DO UPDATE SET (change_date, sequence, state, creation_date, resource_owner, instance_id, id, is_default, hide_login_name_suffix, font_url, watermark_disabled, should_error_popup, light_primary_color, light_warn_color, light_background_color, light_font_color, light_logo_url, light_icon_url, dark_primary_color, dark_warn_color, dark_background_color, dark_font_color, dark_logo_url, dark_icon_url, theme_mode) = ($1, $2, $3, EXCLUDED.creation_date, EXCLUDED.resource_owner, EXCLUDED.instance_id, EXCLUDED.id, EXCLUDED.is_default, EXCLUDED.hide_login_name_suffix, EXCLUDED.font_url, EXCLUDED.watermark_disabled, EXCLUDED.should_error_popup, EXCLUDED.light_primary_color, EXCLUDED.light_warn_color, EXCLUDED.light_background_color, EXCLUDED.light_font_color, EXCLUDED.light_logo_url, EXCLUDED.light_icon_url, EXCLUDED.dark_primary_color, EXCLUDED.dark_warn_color, EXCLUDED.dark_background_color, EXCLUDED.dark_font_color, EXCLUDED.dark_logo_url, EXCLUDED.dark_icon_url, EXCLUDED.theme_mode)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -661,7 +665,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -692,7 +696,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -723,7 +727,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -754,7 +758,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -785,7 +789,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -816,7 +820,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_logo_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -847,7 +851,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -878,7 +882,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, dark_icon_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -909,7 +913,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -940,7 +944,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, font_url) = ($1, $2, $3) WHERE (id = $4) AND (state = $5) AND (instance_id = $6)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -971,7 +975,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.label_policies2 SET (change_date, sequence, light_logo_url, light_icon_url, dark_logo_url, dark_icon_url, font_url) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (state = $9) AND (instance_id = $10)", + expectedStmt: "UPDATE projections.label_policies3 SET (change_date, sequence, light_logo_url, light_icon_url, dark_logo_url, dark_icon_url, font_url) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (state = $9) AND (instance_id = $10)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -1006,7 +1010,7 @@ func TestLabelPolicyProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.label_policies2 WHERE (instance_id = $1) AND (resource_owner = $2)", + expectedStmt: "DELETE FROM projections.label_policies3 WHERE (instance_id = $1) AND (resource_owner = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", diff --git a/internal/repository/instance/policy_label.go b/internal/repository/instance/policy_label.go index 6b14b1cef0..f76013b3ed 100644 --- a/internal/repository/instance/policy_label.go +++ b/internal/repository/instance/policy_label.go @@ -3,6 +3,7 @@ package instance import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/policy" ) @@ -45,6 +46,7 @@ func NewLabelPolicyAddedEvent( hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, + themeMode domain.LabelPolicyThemeMode, ) *LabelPolicyAddedEvent { return &LabelPolicyAddedEvent{ LabelPolicyAddedEvent: *policy.NewLabelPolicyAddedEvent( @@ -62,7 +64,8 @@ func NewLabelPolicyAddedEvent( fontColorDark, hideLoginNameSuffix, errorMsgPopup, - disableWatermark), + disableWatermark, + themeMode), } } diff --git a/internal/repository/org/policy_label.go b/internal/repository/org/policy_label.go index 2ca2c874ba..a467b6f5e4 100644 --- a/internal/repository/org/policy_label.go +++ b/internal/repository/org/policy_label.go @@ -3,6 +3,7 @@ package org import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/policy" ) @@ -46,6 +47,7 @@ func NewLabelPolicyAddedEvent( hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, + themeMode domain.LabelPolicyThemeMode, ) *LabelPolicyAddedEvent { return &LabelPolicyAddedEvent{ LabelPolicyAddedEvent: *policy.NewLabelPolicyAddedEvent( @@ -63,7 +65,8 @@ func NewLabelPolicyAddedEvent( fontColorDark, hideLoginNameSuffix, errorMsgPopup, - disableWatermark), + disableWatermark, + themeMode), } } diff --git a/internal/repository/policy/label.go b/internal/repository/policy/label.go index 39d6379180..d3025b5ba0 100644 --- a/internal/repository/policy/label.go +++ b/internal/repository/policy/label.go @@ -1,6 +1,7 @@ package policy import ( + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/asset" @@ -32,17 +33,18 @@ const ( type LabelPolicyAddedEvent struct { eventstore.BaseEvent `json:"-"` - PrimaryColor string `json:"primaryColor,omitempty"` - BackgroundColor string `json:"backgroundColor,omitempty"` - WarnColor string `json:"warnColor,omitempty"` - FontColor string `json:"fontColor,omitempty"` - PrimaryColorDark string `json:"primaryColorDark,omitempty"` - BackgroundColorDark string `json:"backgroundColorDark,omitempty"` - WarnColorDark string `json:"warnColorDark,omitempty"` - FontColorDark string `json:"fontColorDark,omitempty"` - HideLoginNameSuffix bool `json:"hideLoginNameSuffix,omitempty"` - ErrorMsgPopup bool `json:"errorMsgPopup,omitempty"` - DisableWatermark bool `json:"disableMsgPopup,omitempty"` + PrimaryColor string `json:"primaryColor,omitempty"` + BackgroundColor string `json:"backgroundColor,omitempty"` + WarnColor string `json:"warnColor,omitempty"` + FontColor string `json:"fontColor,omitempty"` + PrimaryColorDark string `json:"primaryColorDark,omitempty"` + BackgroundColorDark string `json:"backgroundColorDark,omitempty"` + WarnColorDark string `json:"warnColorDark,omitempty"` + FontColorDark string `json:"fontColorDark,omitempty"` + HideLoginNameSuffix bool `json:"hideLoginNameSuffix,omitempty"` + ErrorMsgPopup bool `json:"errorMsgPopup,omitempty"` + DisableWatermark bool `json:"disableMsgPopup,omitempty"` + ThemeMode domain.LabelPolicyThemeMode `json:"themeMode,omitempty"` } func (e *LabelPolicyAddedEvent) Payload() interface{} { @@ -66,6 +68,7 @@ func NewLabelPolicyAddedEvent( hideLoginNameSuffix, errorMsgPopup, disableWatermark bool, + themeMode domain.LabelPolicyThemeMode, ) *LabelPolicyAddedEvent { return &LabelPolicyAddedEvent{ @@ -81,6 +84,7 @@ func NewLabelPolicyAddedEvent( HideLoginNameSuffix: hideLoginNameSuffix, ErrorMsgPopup: errorMsgPopup, DisableWatermark: disableWatermark, + ThemeMode: themeMode, } } @@ -100,17 +104,18 @@ func LabelPolicyAddedEventMapper(event eventstore.Event) (eventstore.Event, erro type LabelPolicyChangedEvent struct { eventstore.BaseEvent `json:"-"` - PrimaryColor *string `json:"primaryColor,omitempty"` - BackgroundColor *string `json:"backgroundColor,omitempty"` - WarnColor *string `json:"warnColor,omitempty"` - FontColor *string `json:"fontColor,omitempty"` - PrimaryColorDark *string `json:"primaryColorDark,omitempty"` - BackgroundColorDark *string `json:"backgroundColorDark,omitempty"` - WarnColorDark *string `json:"warnColorDark,omitempty"` - FontColorDark *string `json:"fontColorDark,omitempty"` - HideLoginNameSuffix *bool `json:"hideLoginNameSuffix,omitempty"` - ErrorMsgPopup *bool `json:"errorMsgPopup,omitempty"` - DisableWatermark *bool `json:"disableWatermark,omitempty"` + PrimaryColor *string `json:"primaryColor,omitempty"` + BackgroundColor *string `json:"backgroundColor,omitempty"` + WarnColor *string `json:"warnColor,omitempty"` + FontColor *string `json:"fontColor,omitempty"` + PrimaryColorDark *string `json:"primaryColorDark,omitempty"` + BackgroundColorDark *string `json:"backgroundColorDark,omitempty"` + WarnColorDark *string `json:"warnColorDark,omitempty"` + FontColorDark *string `json:"fontColorDark,omitempty"` + HideLoginNameSuffix *bool `json:"hideLoginNameSuffix,omitempty"` + ErrorMsgPopup *bool `json:"errorMsgPopup,omitempty"` + DisableWatermark *bool `json:"disableWatermark,omitempty"` + ThemeMode *domain.LabelPolicyThemeMode `json:"themeMode,omitempty"` } func (e *LabelPolicyChangedEvent) Payload() interface{} { @@ -205,6 +210,12 @@ func ChangeDisableWatermark(disableWatermark bool) func(*LabelPolicyChangedEvent } } +func ChangeThemeMode(themeMode domain.LabelPolicyThemeMode) func(*LabelPolicyChangedEvent) { + return func(e *LabelPolicyChangedEvent) { + e.ThemeMode = &themeMode + } +} + func LabelPolicyChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { e := &LabelPolicyChangedEvent{ BaseEvent: *eventstore.BaseEventFromRepo(event), diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index f750c4d27f..1a67f03a4f 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -6230,6 +6230,11 @@ message UpdateLabelPolicyRequest { } ]; bool disable_watermark = 11; + zitadel.policy.v1.ThemeMode theme_mode = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "setting if there should be a restriction on which themes are available"; + } + ]; } message UpdateLabelPolicyResponse { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 3c485f7a92..53caf0c2af 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -10583,6 +10583,11 @@ message AddCustomLabelPolicyRequest { } ]; bool disable_watermark = 11; + zitadel.policy.v1.ThemeMode theme_mode = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "setting if there should be a restriction on which themes are available"; + } + ]; } message AddCustomLabelPolicyResponse { @@ -10653,6 +10658,11 @@ message UpdateCustomLabelPolicyRequest { } ]; bool disable_watermark = 11; + zitadel.policy.v1.ThemeMode theme_mode = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "setting if there should be a restriction on which themes are available"; + } + ]; } message UpdateCustomLabelPolicyResponse { diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto index 4b3aa9035e..dc1c0cf9e0 100644 --- a/proto/zitadel/policy.proto +++ b/proto/zitadel/policy.proto @@ -145,6 +145,14 @@ message LabelPolicy { } ]; string font_url = 18; + ThemeMode theme_mode = 19; +} + +enum ThemeMode { + THEME_MODE_UNSPECIFIED = 0; + THEME_MODE_AUTO = 1; + THEME_MODE_DARK = 2; + THEME_MODE_LIGHT = 3; } message LoginPolicy { diff --git a/proto/zitadel/settings/v2beta/branding_settings.proto b/proto/zitadel/settings/v2beta/branding_settings.proto index 5ddc4067ce..4c50847f48 100644 --- a/proto/zitadel/settings/v2beta/branding_settings.proto +++ b/proto/zitadel/settings/v2beta/branding_settings.proto @@ -33,6 +33,11 @@ message BrandingSettings { description: "resource_owner_type returns if the setting is managed on the organization or on the instance"; } ]; + ThemeMode theme_mode = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "states whether both or only dark or light theme will be used"; + } + ]; } message Theme { @@ -79,3 +84,10 @@ message Theme { } ]; } + +enum ThemeMode { + THEME_MODE_UNSPECIFIED = 0; + THEME_MODE_AUTO = 1; + THEME_MODE_LIGHT = 2; + THEME_MODE_DARK = 3; +} \ No newline at end of file From b099a26a1600be6661d9358c2ffbea034e47b51c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 26 Oct 2023 10:29:06 +0200 Subject: [PATCH 39/48] feat(console): MDC components (#6482) mdc components --------- Co-authored-by: Elio Bischof --- cmd/ready/ready.go | 7 +- console/angular.json | 1 + console/package.json | 42 +- console/src/app/app.component.scss | 8 +- console/src/app/app.component.ts | 1 - console/src/app/app.module.ts | 8 +- .../accounts-card.component.scss | 12 +- .../accounts-card/accounts-card.module.ts | 4 +- .../action-keys/action-keys.component.scss | 6 +- .../add-key-dialog.component.ts | 5 +- .../add-key-dialog/add-key-dialog.module.ts | 6 +- .../member-create-dialog.component.html | 3 +- .../member-create-dialog.component.scss | 1 - .../member-create-dialog.component.ts | 5 +- .../member-create-dialog.module.ts | 12 +- .../add-member-roles-dialog.component.scss | 8 +- .../add-member-roles-dialog.component.ts | 5 +- .../add-member-roles-dialog.module.ts | 8 +- .../add-token-dialog.component.ts | 5 +- .../add-token-dialog.module.ts | 6 +- .../modules/app-card/app-card.component.scss | 6 +- .../app-auth-method-radio.component.scss | 4 +- .../app-type-radio.component.scss | 4 +- .../app/modules/avatar/avatar.component.scss | 4 +- console/src/app/modules/card/card.module.ts | 4 +- console/src/app/modules/card/card.scss | 13 +- .../modules/changes/changes.component.scss | 2 - .../src/app/modules/changes/changes.module.ts | 6 +- .../client-keys/client-keys.component.html | 4 +- .../client-keys/client-keys.component.ts | 4 +- .../modules/client-keys/client-keys.module.ts | 12 +- .../contributors/contributors.component.scss | 2 - .../contributors/contributors.module.ts | 6 +- .../create-layout.component.scss | 10 +- .../create-layout/create-layout.module.ts | 4 +- .../detail-layout.component.scss | 6 +- .../detail-layout/detail-layout.module.ts | 2 +- .../display-json-dialog.component.ts | 5 +- .../display-json-dialog.module.ts | 4 +- .../add-domain-dialog.component.html | 2 +- .../add-domain-dialog.component.scss | 4 - .../add-domain-dialog.component.ts | 5 +- .../add-domain-dialog.module.ts | 5 +- .../domain-verification.component.ts | 5 +- .../modules/domains/domains.component.html | 10 +- .../modules/domains/domains.component.scss | 2 +- .../app/modules/domains/domains.component.ts | 3 +- .../src/app/modules/domains/domains.module.ts | 6 +- .../edit-text/edit-text.component.scss | 10 +- .../app/modules/edit-text/edit-text.module.ts | 4 +- .../filter-events.component.html | 17 +- .../filter-events.component.scss | 12 +- .../filter-events/filter-events.module.ts | 12 +- .../filter-org/filter-org.component.ts | 2 +- .../modules/filter-org/filter-org.module.ts | 6 +- .../filter-project.component.ts | 2 +- .../filter-project/filter-project.module.ts | 6 +- .../filter-user-grants.component.ts | 2 +- .../filter-user-grants.module.ts | 6 +- .../filter-user/filter-user.component.ts | 2 +- .../modules/filter-user/filter-user.module.ts | 6 +- .../app/modules/filter/filter.component.html | 20 +- .../app/modules/filter/filter.component.scss | 14 +- .../src/app/modules/filter/filter.module.ts | 6 +- .../app/modules/footer/footer.component.scss | 21 +- .../field/form-field.component.scss | 15 +- .../form-field/field/form-field.component.ts | 2 +- .../app/modules/header/header.component.html | 16 +- .../app/modules/header/header.component.scss | 8 +- .../src/app/modules/header/header.module.ts | 4 +- .../idp-table/idp-table.component.scss | 6 +- .../modules/idp-table/idp-table.component.ts | 4 +- .../app/modules/idp-table/idp-table.module.ts | 8 +- .../info-dialog/info-dialog.component.ts | 5 +- .../modules/info-dialog/info-dialog.module.ts | 5 +- .../info-overlay/info-overlay.component.scss | 6 +- .../info-overlay/info-overlay.module.ts | 2 +- .../modules/info-row/info-row.component.scss | 2 - .../app/modules/info-row/info-row.module.ts | 4 +- .../info-section/info-section.component.scss | 4 +- .../src/app/modules/input/input.directive.ts | 13 +- .../keyboard-shortcuts.component.html | 2 +- .../keyboard-shortcuts.component.scss | 6 +- .../keyboard-shortcuts.component.ts | 5 +- .../keyboard-shortcuts.module.ts | 2 +- .../app/modules/label/label.component.scss | 6 +- .../machine-keys/machine-keys.component.html | 4 +- .../machine-keys/machine-keys.component.ts | 4 +- .../machine-keys/machine-keys.module.ts | 12 +- .../members-table.component.html | 90 +-- .../members-table.component.spec.ts | 2 +- .../members-table/members-table.component.ts | 4 +- .../members-table/members-table.module.ts | 12 +- .../memberships-table.component.html | 64 +- .../memberships-table.component.spec.ts | 2 +- .../memberships-table.component.ts | 2 +- .../memberships-table.module.ts | 14 +- .../modules/meta-layout/meta-layout.module.ts | 2 +- .../metadata-dialog.component.html | 12 +- .../metadata-dialog.component.ts | 6 +- .../app/modules/metadata/metadata.module.ts | 12 +- .../metadata/metadata/metadata.component.ts | 2 +- .../name-dialog/name-dialog.component.html | 5 +- .../name-dialog/name-dialog.component.ts | 5 +- .../modules/name-dialog/name-dialog.module.ts | 4 +- .../nav-toggle/nav-toggle.component.scss | 12 +- .../src/app/modules/nav/nav.component.html | 17 +- .../src/app/modules/nav/nav.component.scss | 34 +- console/src/app/modules/nav/nav.module.ts | 12 +- .../onboarding-card.component.scss | 6 +- .../onboarding-card/onboarding-card.module.ts | 4 +- .../onboarding/onboarding.component.scss | 23 +- .../modules/onboarding/onboarding.module.ts | 8 +- .../org-context/org-context.component.html | 6 +- .../org-context/org-context.component.scss | 15 +- .../modules/org-context/org-context.module.ts | 6 +- .../org-table/org-table.component.html | 39 +- .../org-table/org-table.component.scss | 25 +- .../modules/org-table/org-table.component.ts | 2 +- .../app/modules/org-table/org-table.module.ts | 8 +- .../paginator/paginator.component.scss | 4 +- .../app/modules/paginator/paginator.module.ts | 4 +- .../password-complexity-view.module.ts | 2 +- .../personal-access-tokens.component.html | 4 +- .../personal-access-tokens.component.ts | 4 +- .../personal-access-tokens.module.ts | 12 +- .../domain-policy/domain-policy.component.ts | 2 +- .../domain-policy/domain-policy.module.ts | 10 +- .../general-settings.module.ts | 6 +- .../idp-settings/idp-settings.component.scss | 4 +- .../idp-settings/idp-settings.module.ts | 6 +- .../dialog-add-type.component.ts | 5 +- .../factor-table/factor-table.component.html | 8 +- .../factor-table/factor-table.component.scss | 2 - .../factor-table/factor-table.component.ts | 4 +- .../login-policy/login-policy.component.ts | 2 +- .../login-policy/login-policy.module.ts | 14 +- .../login-texts/login-texts.component.html | 11 +- .../login-texts/login-texts.component.scss | 23 +- .../login-texts/login-texts.component.ts | 2 +- .../login-texts/login-texts.module.ts | 10 +- .../message-texts.component.scss | 6 +- .../message-texts/message-texts.component.ts | 4 +- .../message-texts/message-texts.module.ts | 8 +- .../notification-policy.component.ts | 2 +- .../notification-policy.module.ts | 10 +- .../dialog-add-sms-provider.component.ts | 6 +- .../notification-sms-provider.component.ts | 2 +- .../notification-sms-provider.module.ts | 10 +- .../password-dialog.component.ts | 5 +- .../notification-smtp-provider.component.ts | 2 +- .../notification-smtp-provider.module.ts | 10 +- .../oidc-configuration.module.ts | 6 +- .../password-complexity-policy.component.ts | 2 +- .../password-complexity-policy.module.ts | 10 +- .../password-lockout-policy.component.ts | 2 +- .../password-lockout-policy.module.ts | 8 +- .../privacy-policy.component.ts | 2 +- .../privacy-policy/privacy-policy.module.ts | 10 +- .../preview/preview.component.scss | 4 +- .../private-labeling-policy.component.scss | 12 +- .../private-labeling-policy.component.ts | 2 +- .../private-labeling-policy.module.ts | 12 +- .../dialog-add-secret-generator.component.ts | 5 +- .../secret-generator.component.ts | 2 +- .../secret-generator.module.ts | 8 +- .../security-policy.component.scss | 4 +- .../security-policy/security-policy.module.ts | 4 +- .../project-members.component.html | 2 +- .../project-members.component.scss | 2 +- .../project-members.component.spec.ts | 2 +- .../project-members.component.ts | 4 +- .../project-members/project-members.module.ts | 6 +- ...oject-private-labeling-dialog.component.ts | 5 +- .../project-private-labeling-dialog.module.ts | 7 +- .../project-role-chip.component.scss | 27 +- .../project-role-chip.module.ts | 2 +- .../project-role-detail-dialog.component.ts | 5 +- .../project-role-detail-dialog.module.ts | 6 +- .../project-roles-table.component.html | 50 +- .../project-roles-table.component.spec.ts | 2 +- .../project-roles-table.component.ts | 4 +- .../project-roles-table.module.ts | 14 +- .../provider-options.module.ts | 2 +- .../provider-apple.component.html | 14 +- .../provider-apple.component.ts | 2 +- .../provider-azure-ad.component.html | 14 +- .../provider-azure-ad.component.ts | 2 +- .../provider-github-es.component.html | 14 +- .../provider-github-es.component.ts | 2 +- .../provider-github.component.html | 14 +- .../provider-github.component.ts | 2 +- ...provider-gitlab-self-hosted.component.html | 14 +- .../provider-gitlab-self-hosted.component.ts | 2 +- .../provider-gitlab.component.html | 14 +- .../provider-gitlab.component.ts | 2 +- .../provider-google.component.html | 14 +- .../provider-google.component.ts | 2 +- .../provider-oauth.component.html | 14 +- .../provider-oauth.component.ts | 2 +- .../provider-oidc.component.html | 14 +- .../provider-oidc/provider-oidc.component.ts | 2 +- .../app/modules/providers/providers.module.ts | 14 +- .../src/app/modules/providers/providers.scss | 5 +- .../refresh-table.component.html | 20 +- .../refresh-table.component.scss | 4 +- .../refresh-table/refresh-table.module.ts | 6 +- .../search-org-autocomplete.component.ts | 4 +- .../search-org-autocomplete.module.ts | 10 +- .../search-project-autocomplete.component.ts | 8 +- .../search-project-autocomplete.module.ts | 10 +- .../search-roles-autocomplete.component.html | 14 +- .../search-roles-autocomplete.component.ts | 8 +- .../search-roles-autocomplete.module.ts | 8 +- .../search-user-autocomplete.component.scss | 10 +- .../search-user-autocomplete.component.ts | 8 +- .../search-user-autocomplete.module.ts | 12 +- .../settings-grid/settings-grid.module.ts | 4 +- .../shortcuts/shortcuts.component.scss | 12 +- .../app/modules/shortcuts/shortcuts.module.ts | 4 +- .../show-key-dialog.component.ts | 5 +- .../show-key-dialog/show-key-dialog.module.ts | 4 +- .../show-token-dialog.component.ts | 5 +- .../show-token-dialog.module.ts | 6 +- .../modules/sidenav/sidenav.component.scss | 3 - .../string-list/string-list.component.scss | 4 +- .../modules/string-list/string-list.module.ts | 12 +- .../table-actions.component.html | 4 +- .../table-actions.component.scss | 16 +- .../table-actions/table-actions.module.ts | 6 +- .../theme-setting.component.html | 47 +- .../theme-setting.component.scss | 53 +- .../theme-setting/theme-setting.module.ts | 4 +- .../modules/top-view/top-view.component.html | 10 +- .../modules/top-view/top-view.component.scss | 7 +- .../app/modules/top-view/top-view.module.ts | 6 +- .../user-grant-role-dialog.component.ts | 5 +- .../user-grant-role-dialog.module.ts | 13 +- .../user-grants/user-grants.component.html | 109 ++-- .../user-grants/user-grants.component.scss | 2 - .../user-grants/user-grants.component.ts | 6 +- .../modules/user-grants/user-grants.module.ts | 12 +- .../warn-dialog/warn-dialog.component.html | 14 +- .../warn-dialog/warn-dialog.component.scss | 6 +- .../warn-dialog/warn-dialog.component.ts | 5 +- .../modules/warn-dialog/warn-dialog.module.ts | 5 +- .../action-table/action-table.component.html | 47 +- .../action-table/action-table.component.scss | 4 - .../action-table/action-table.component.ts | 6 +- .../app/pages/actions/actions.component.html | 16 +- .../app/pages/actions/actions.component.scss | 2 - .../app/pages/actions/actions.component.ts | 2 +- .../src/app/pages/actions/actions.module.ts | 12 +- .../add-action-dialog.component.scss | 9 - .../add-action-dialog.component.ts | 6 +- .../add-flow-dialog.component.html | 1 + .../add-flow-dialog.component.scss | 5 +- .../add-flow-dialog.component.ts | 5 +- .../app-create/app-create.component.scss | 4 +- .../app/pages/app-create/app-create.module.ts | 8 +- .../app/pages/events/events.component.scss | 6 +- .../src/app/pages/events/events.component.ts | 4 +- console/src/app/pages/events/events.module.ts | 12 +- .../failed-events/failed-events.component.ts | 4 +- .../failed-events/failed-events.module.ts | 8 +- .../src/app/pages/home/home.component.scss | 12 +- console/src/app/pages/home/home.module.ts | 6 +- .../pages/iam-views/iam-views.component.ts | 4 +- .../app/pages/iam-views/iam-views.module.ts | 8 +- .../instance-members.component.html | 12 +- .../instance-members.component.scss | 2 +- .../instance-members.component.spec.ts | 2 +- .../instance-members.component.ts | 4 +- .../instance-members.module.ts | 4 +- .../pages/instance/instance.component.scss | 2 - .../app/pages/instance/instance.component.ts | 2 +- .../src/app/pages/instance/instance.module.ts | 16 +- .../org-create/org-create.component.scss | 6 +- .../pages/org-create/org-create.component.ts | 2 +- .../app/pages/org-create/org-create.module.ts | 8 +- .../orgs/org-detail/org-detail.component.ts | 2 +- .../org-members/org-members.component.html | 12 +- .../org-members/org-members.component.scss | 2 +- .../org-members/org-members.component.spec.ts | 2 +- .../orgs/org-members/org-members.component.ts | 4 +- .../orgs/org-members/org-members.module.ts | 6 +- console/src/app/pages/orgs/org.module.ts | 10 +- .../additional-origins.component.scss | 2 +- .../apps/app-create/app-create.component.scss | 6 +- .../apps/app-create/app-create.component.ts | 2 +- .../apps/app-detail/app-detail.component.html | 4 +- .../apps/app-detail/app-detail.component.scss | 4 +- .../apps/app-detail/app-detail.component.ts | 13 +- .../auth-method-dialog.component.ts | 5 +- .../app-secret-dialog.component.html | 3 +- .../app-secret-dialog.component.ts | 5 +- .../app/pages/projects/apps/apps.module.ts | 24 +- .../redirect-uris.component.scss | 8 +- .../granted-project-detail.component.scss | 2 - .../granted-project-detail.component.ts | 2 +- .../granted-projects.module.ts | 14 +- .../applications/applications.component.html | 41 +- .../applications.component.spec.ts | 2 +- .../applications/applications.component.ts | 2 +- .../owned-project-detail.component.scss | 2 - .../owned-project-detail.component.ts | 4 +- .../owned-project-detail.module.ts | 14 +- .../owned-projects/owned-projects.module.ts | 14 +- .../project-grant-create.component.scss | 3 +- .../project-grant-create.module.ts | 12 +- .../project-grant-detail.component.html | 26 +- .../project-grant-detail.component.scss | 2 +- .../project-grant-detail.component.ts | 4 +- .../project-grant-detail.module.ts | 20 +- .../project-grants.component.html | 51 +- .../project-grants.component.ts | 6 +- .../project-grants/project-grants.module.ts | 10 +- .../project-role-create.component.scss | 3 +- .../project-role-create.module.ts | 4 +- .../project-roles/project-roles.module.ts | 2 +- .../project-create.component.scss | 4 +- .../project-create/project-create.module.ts | 2 +- .../project-grid/project-grid.component.ts | 2 +- .../project-list/project-list.component.html | 45 +- .../project-list/project-list.component.ts | 4 +- .../pages/projects/projects.component.html | 2 +- .../pages/projects/projects.component.scss | 14 +- .../src/app/pages/projects/projects.module.ts | 12 +- .../pages/signedout/signedout.component.html | 14 +- .../pages/signedout/signedout.component.ts | 4 +- .../app/pages/signedout/signedout.module.ts | 4 +- .../user-grant-create.component.scss | 1 + .../user-grant-create.module.ts | 4 +- .../user-create-machine.component.scss | 1 + .../user-create-machine.module.ts | 12 +- .../user-create/user-create.component.html | 19 +- .../user-create/user-create.component.scss | 41 +- .../users/user-create/user-create.module.ts | 12 +- .../auth-factor-dialog.component.ts | 5 +- .../auth-passwordless.component.html | 8 +- .../auth-passwordless.component.ts | 4 +- .../dialog-passwordless.component.ts | 5 +- .../auth-user-detail.component.scss | 2 - .../auth-user-detail.component.ts | 2 +- .../auth-user-mfa.component.html | 8 +- .../auth-user-mfa/auth-user-mfa.component.ts | 4 +- .../code-dialog/code-dialog.component.ts | 5 +- .../dialog-u2f/dialog-u2f.component.ts | 5 +- .../edit-dialog/edit-dialog.component.ts | 5 +- .../resend-email-dialog.component.ts | 5 +- .../contact/contact.component.scss | 6 - .../user-detail/contact/contact.component.ts | 2 +- .../detail-form-machine.module.ts | 4 +- .../detail-form/detail-form.component.ts | 2 +- .../detail-form/detail-form.module.ts | 6 +- .../profile-picture.component.ts | 5 +- .../external-idps/external-idps.component.ts | 4 +- .../password/password.component.scss | 1 + .../users/user-detail/user-detail.module.ts | 16 +- .../machine-secret-dialog.component.html | 3 +- .../machine-secret-dialog.component.ts | 5 +- .../passwordless/passwordless.component.html | 8 +- .../passwordless/passwordless.component.ts | 4 +- .../user-detail/user-detail.component.scss | 2 - .../user-detail/user-detail.component.ts | 2 +- .../user-mfa/user-mfa.component.ts | 4 +- .../users/user-list/user-list.component.html | 10 +- .../users/user-list/user-list.component.scss | 2 +- .../pages/users/user-list/user-list.module.ts | 14 +- .../user-table/user-table.component.html | 124 ++-- .../user-table/user-table.component.scss | 25 - .../user-table/user-table.component.ts | 11 +- console/src/app/services/exhausted.service.ts | 2 +- console/src/app/services/grpc-auth.service.ts | 1 - console/src/app/services/grpc.service.ts | 5 +- .../services/interceptors/auth.interceptor.ts | 2 +- .../keyboard-shortcuts.service.ts | 10 +- console/src/app/services/theme.service.ts | 68 -- console/src/app/services/toast.service.ts | 8 +- console/src/app/services/update.service.ts | 2 +- console/src/assets/i18n/bg.json | 1 - console/src/assets/i18n/de.json | 1 - console/src/assets/i18n/en.json | 1 - console/src/assets/i18n/es.json | 1 - console/src/assets/i18n/fr.json | 1 - console/src/assets/i18n/it.json | 1 - console/src/assets/i18n/ja.json | 1 - console/src/assets/i18n/mk.json | 1 - console/src/assets/i18n/pl.json | 1 - console/src/assets/i18n/pt.json | 1 - console/src/assets/i18n/zh.json | 1 - console/src/component-themes.scss | 2 - console/src/styles.scss | 506 ++++++++------- console/src/styles/ailerons.scss | 5 + console/src/styles/color.scss | 2 - console/src/styles/error.scss | 4 +- console/src/styles/input.scss | 16 +- console/src/styles/lato-font.scss | 120 ++-- console/src/styles/link.scss | 18 +- console/src/styles/palette-helper.scss | 43 ++ console/src/styles/palette.scss | 134 ++++ console/src/styles/table.scss | 32 +- console/src/styles/toast.scss | 14 +- console/yarn.lock | 589 +++++------------- e2e/config/localhost/docker-compose.yaml | 4 +- e2e/cypress.config.ts | 1 + e2e/cypress/e2e/humans/humans.cy.ts | 7 +- e2e/cypress/e2e/machines/machines.cy.ts | 2 +- e2e/cypress/support/login/authenticate.ts | 31 + e2e/cypress/support/login/users.ts | 63 +- e2e/package-lock.json | 427 ++++++++++--- e2e/package.json | 4 +- 412 files changed, 2711 insertions(+), 2698 deletions(-) create mode 100644 console/src/styles/ailerons.scss create mode 100644 console/src/styles/palette-helper.scss create mode 100644 console/src/styles/palette.scss create mode 100644 e2e/cypress/support/login/authenticate.ts diff --git a/cmd/ready/ready.go b/cmd/ready/ready.go index 2c0e10d076..679a1ac563 100644 --- a/cmd/ready/ready.go +++ b/cmd/ready/ready.go @@ -32,6 +32,9 @@ func ready(config *Config) bool { return false } defer res.Body.Close() - logging.WithFields("status", res.StatusCode).Warn("ready check failed") - return res.StatusCode == 200 + if res.StatusCode != 200 { + logging.WithFields("status", res.StatusCode).Warn("ready check failed") + return false + } + return true } diff --git a/console/angular.json b/console/angular.json index 1301e6686b..ad3d13efd2 100644 --- a/console/angular.json +++ b/console/angular.json @@ -30,6 +30,7 @@ "includePaths": ["node_modules"] }, "allowedCommonJsDependencies": [ + "opentype.js", "fast-sha256", "buffer", "moment", diff --git a/console/package.json b/console/package.json index 5b677e6c25..5819fdd77a 100644 --- a/console/package.json +++ b/console/package.json @@ -12,18 +12,18 @@ }, "private": true, "dependencies": { - "@angular/animations": "^16.2.0", - "@angular/cdk": "^16.2.0", - "@angular/common": "^16.2.0", - "@angular/compiler": "^16.2.0", - "@angular/core": "^16.2.0", - "@angular/forms": "^16.2.0", - "@angular/material": "^16.2.0", - "@angular/material-moment-adapter": "^16.2.0", - "@angular/platform-browser": "^16.2.0", - "@angular/platform-browser-dynamic": "^16.2.0", - "@angular/router": "^16.2.0", - "@angular/service-worker": "^16.2.0", + "@angular/animations": "^16.2.5", + "@angular/cdk": "^16.2.4", + "@angular/common": "^16.2.5", + "@angular/compiler": "^16.2.5", + "@angular/core": "^16.2.5", + "@angular/forms": "^16.2.5", + "@angular/material": "^16.2.4", + "@angular/material-moment-adapter": "^16.2.4", + "@angular/platform-browser": "^16.2.5", + "@angular/platform-browser-dynamic": "^16.2.5", + "@angular/router": "^16.2.5", + "@angular/service-worker": "^16.2.5", "@ctrl/ngx-codemirror": "^6.1.0", "@grpc/grpc-js": "^1.9.3", "@ngx-translate/core": "^14.0.0", @@ -50,15 +50,15 @@ "zone.js": "~0.13.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.2.0", - "@angular-eslint/builder": "16.1.0", - "@angular-eslint/eslint-plugin": "16.1.0", - "@angular-eslint/eslint-plugin-template": "16.1.0", - "@angular-eslint/schematics": "16.1.0", - "@angular-eslint/template-parser": "16.1.0", - "@angular/cli": "^16.2.0", - "@angular/compiler-cli": "^16.2.0", - "@angular/language-service": "^16.2.0", + "@angular-devkit/build-angular": "^16.2.2", + "@angular-eslint/builder": "16.2.0", + "@angular-eslint/eslint-plugin": "16.2.0", + "@angular-eslint/eslint-plugin-template": "16.2.0", + "@angular-eslint/schematics": "16.2.0", + "@angular-eslint/template-parser": "16.2.0", + "@angular/cli": "^16.2.2", + "@angular/compiler-cli": "^16.2.5", + "@angular/language-service": "^16.2.5", "@bufbuild/buf": "^1.23.1", "@types/file-saver": "^2.0.2", "@types/google-protobuf": "^3.15.3", diff --git a/console/src/app/app.component.scss b/console/src/app/app.component.scss index b3a8cd86f4..865b96e3f8 100644 --- a/console/src/app/app.component.scss +++ b/console/src/app/app.component.scss @@ -1,15 +1,13 @@ -@use '@angular/material' as mat; - @mixin main-theme($theme) { $primary: map-get($theme, primary); $warn: map-get($theme, warn); $background: map-get($theme, background); $foreground: map-get($theme, foreground); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $is-dark-theme: map-get($theme, is-dark); .main-container { diff --git a/console/src/app/app.component.ts b/console/src/app/app.component.ts index b4758e180e..1a9ef8cd91 100644 --- a/console/src/app/app.component.ts +++ b/console/src/app/app.component.ts @@ -218,7 +218,6 @@ export class AppComponent implements OnDestroy { }) .catch((error) => { console.error(error); - this.themeService.setDefaultColors(); this.router.navigate(['/users/me']); }); } diff --git a/console/src/app/app.module.ts b/console/src/app/app.module.ts index 3781c33576..68c356ff8c 100644 --- a/console/src/app/app.module.ts +++ b/console/src/app/app.module.ts @@ -13,11 +13,11 @@ import localePt from '@angular/common/locales/pt'; import localeZh from '@angular/common/locales/zh'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { MatNativeDateModule } from '@angular/material/core'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ServiceWorkerModule } from '@angular/service-worker'; diff --git a/console/src/app/modules/accounts-card/accounts-card.component.scss b/console/src/app/modules/accounts-card/accounts-card.component.scss index f0a1e30f74..c4926ca891 100644 --- a/console/src/app/modules/accounts-card/accounts-card.component.scss +++ b/console/src/app/modules/accounts-card/accounts-card.component.scss @@ -3,11 +3,11 @@ $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); - $card-background-color: mat.get-color-from-palette($background, cards); + $primary-color: map-get($primary, 500); + $card-background-color: map-get($background, cards); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); @@ -54,10 +54,6 @@ button { border-radius: 50vh; margin: 0.5rem; - - .mat-button-wrapper { - font-size: 0.8rem; - } } .l-accounts { diff --git a/console/src/app/modules/accounts-card/accounts-card.module.ts b/console/src/app/modules/accounts-card/accounts-card.module.ts index 7f4ec08ae8..057175a793 100644 --- a/console/src/app/modules/accounts-card/accounts-card.module.ts +++ b/console/src/app/modules/accounts-card/accounts-card.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressBarModule as MatProgressBarModule } from '@angular/material/legacy-progress-bar'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; diff --git a/console/src/app/modules/action-keys/action-keys.component.scss b/console/src/app/modules/action-keys/action-keys.component.scss index 824e763ec1..102019f9ca 100644 --- a/console/src/app/modules/action-keys/action-keys.component.scss +++ b/console/src/app/modules/action-keys/action-keys.component.scss @@ -1,12 +1,10 @@ -@use '@angular/material' as mat; - @mixin action-keys-theme($theme) { $primary: map-get($theme, primary); $background: map-get($theme, background); $foreground: map-get($theme, foreground); $accent: map-get($theme, accent); $is-dark-theme: map-get($theme, is-dark); - $accent-color: mat.get-color-from-palette($primary, 500); + $accent-color: map-get($primary, 500); $back: map-get($background, background); .action-keys-wrapper { @@ -45,7 +43,7 @@ right: 0; bottom: 0; left: 0; - background: mat.get-color-from-palette($primary, default-contrast); + background: map-get($primary, default-contrast); opacity: 0.2; border-radius: 4px; } diff --git a/console/src/app/modules/add-key-dialog/add-key-dialog.component.ts b/console/src/app/modules/add-key-dialog/add-key-dialog.component.ts index c072b974b0..4e5c7f6de6 100644 --- a/console/src/app/modules/add-key-dialog/add-key-dialog.component.ts +++ b/console/src/app/modules/add-key-dialog/add-key-dialog.component.ts @@ -1,9 +1,6 @@ import { Component, Inject } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { KeyType } from 'src/app/proto/generated/zitadel/auth_n_key_pb'; export enum AddKeyDialogType { diff --git a/console/src/app/modules/add-key-dialog/add-key-dialog.module.ts b/console/src/app/modules/add-key-dialog/add-key-dialog.module.ts index 4301a8bbc2..1dd50fae2a 100644 --- a/console/src/app/modules/add-key-dialog/add-key-dialog.module.ts +++ b/console/src/app/modules/add-key-dialog/add-key-dialog.module.ts @@ -2,14 +2,15 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatMomentDateModule } from '@angular/material-moment-adapter'; +import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { MatSelectModule } from '@angular/material/select'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; +import { MatDialogModule } from '@angular/material/dialog'; import { AddKeyDialogComponent } from './add-key-dialog.component'; @NgModule({ @@ -22,6 +23,7 @@ import { AddKeyDialogComponent } from './add-key-dialog.component'; MatSelectModule, MatIconModule, FormsModule, + MatDialogModule, MatDatepickerModule, MatMomentDateModule, ReactiveFormsModule, diff --git a/console/src/app/modules/add-member-dialog/member-create-dialog.component.html b/console/src/app/modules/add-member-dialog/member-create-dialog.component.html index 3cfcefdeaf..679d27f2bd 100644 --- a/console/src/app/modules/add-member-dialog/member-create-dialog.component.html +++ b/console/src/app/modules/add-member-dialog/member-create-dialog.component.html @@ -1,9 +1,10 @@

{{ 'MEMBER.ADD' | translate }}

-

{{ 'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate }}

+

{{ 'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate }}

+ {{ 'MEMBER.CREATIONTYPE' | translate }} diff --git a/console/src/app/modules/add-member-dialog/member-create-dialog.component.scss b/console/src/app/modules/add-member-dialog/member-create-dialog.component.scss index 0336217b1f..ec0bc2f3d3 100644 --- a/console/src/app/modules/add-member-dialog/member-create-dialog.component.scss +++ b/console/src/app/modules/add-member-dialog/member-create-dialog.component.scss @@ -14,7 +14,6 @@ box-sizing: border-box; .role-cb { - padding: 0.25rem 0; box-sizing: border-box; .role-cb-content { diff --git a/console/src/app/modules/add-member-dialog/member-create-dialog.component.ts b/console/src/app/modules/add-member-dialog/member-create-dialog.component.ts index c90477852e..6e34450b56 100644 --- a/console/src/app/modules/add-member-dialog/member-create-dialog.component.ts +++ b/console/src/app/modules/add-member-dialog/member-create-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Observable } from 'rxjs'; import { GrantedProject, Project } from 'src/app/proto/generated/zitadel/project_pb'; import { User } from 'src/app/proto/generated/zitadel/user_pb'; diff --git a/console/src/app/modules/add-member-dialog/member-create-dialog.module.ts b/console/src/app/modules/add-member-dialog/member-create-dialog.module.ts index ec9b6204d5..a274c173c2 100644 --- a/console/src/app/modules/add-member-dialog/member-create-dialog.module.ts +++ b/console/src/app/modules/add-member-dialog/member-create-dialog.module.ts @@ -1,12 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module'; diff --git a/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.scss b/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.scss index e1c7139205..f69be987e7 100644 --- a/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.scss +++ b/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.scss @@ -12,8 +12,6 @@ flex-direction: column; .role-cb { - padding: 0.25rem 0; - .role-cb-content { padding-left: 0.5rem; display: flex; @@ -30,5 +28,9 @@ .action { display: flex; - justify-content: space-between; + justify-content: flex-end; + + .ok-button { + margin-left: 1rem; + } } diff --git a/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.ts b/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.ts index 538b8d307f..8119d541c8 100644 --- a/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.ts +++ b/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { getMembershipColor } from 'src/app/utils/color'; @Component({ diff --git a/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.module.ts b/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.module.ts index e6546c9a2c..3bc964bff9 100644 --- a/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.module.ts +++ b/console/src/app/modules/add-member-roles-dialog/add-member-roles-dialog.module.ts @@ -1,13 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { RoleTransformPipeModule } from 'src/app/pipes/role-transform/role-transform.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; +import { MatDialogModule } from '@angular/material/dialog'; import { AddMemberRolesDialogComponent } from './add-member-roles-dialog.component'; @NgModule({ @@ -17,6 +18,7 @@ import { AddMemberRolesDialogComponent } from './add-member-roles-dialog.compone TranslateModule, MatCheckboxModule, MatButtonModule, + MatDialogModule, LocalizedDatePipeModule, MatTooltipModule, RoleTransformPipeModule, diff --git a/console/src/app/modules/add-token-dialog/add-token-dialog.component.ts b/console/src/app/modules/add-token-dialog/add-token-dialog.component.ts index d41cb3fc2f..fe84a02db4 100644 --- a/console/src/app/modules/add-token-dialog/add-token-dialog.component.ts +++ b/console/src/app/modules/add-token-dialog/add-token-dialog.component.ts @@ -1,9 +1,6 @@ import { Component, Inject } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @Component({ selector: 'cnsl-add-token-dialog', diff --git a/console/src/app/modules/add-token-dialog/add-token-dialog.module.ts b/console/src/app/modules/add-token-dialog/add-token-dialog.module.ts index 99334f458c..7503acc66b 100644 --- a/console/src/app/modules/add-token-dialog/add-token-dialog.module.ts +++ b/console/src/app/modules/add-token-dialog/add-token-dialog.module.ts @@ -2,14 +2,15 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatMomentDateModule } from '@angular/material-moment-adapter'; +import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { MatSelectModule } from '@angular/material/select'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; +import { MatDialogModule } from '@angular/material/dialog'; import { InfoSectionModule } from '../info-section/info-section.module'; import { AddTokenDialogComponent } from './add-token-dialog.component'; @@ -22,6 +23,7 @@ import { AddTokenDialogComponent } from './add-token-dialog.component'; InfoSectionModule, InputModule, MatSelectModule, + MatDialogModule, MatIconModule, FormsModule, MatDatepickerModule, diff --git a/console/src/app/modules/app-card/app-card.component.scss b/console/src/app/modules/app-card/app-card.component.scss index f34ebeeec5..ce878502b4 100644 --- a/console/src/app/modules/app-card/app-card.component.scss +++ b/console/src/app/modules/app-card/app-card.component.scss @@ -1,12 +1,10 @@ -@use '@angular/material' as mat; - @mixin app-card-theme($theme) { $primary: map-get($theme, primary); $background: map-get($theme, background); $foreground: map-get($theme, foreground); $accent: map-get($theme, accent); $is-dark-theme: map-get($theme, is-dark); - $accent-color: mat.get-color-from-palette($primary, 500); + $accent-color: map-get($primary, 500); $back: map-get($background, background); .cnsl-app-card { @@ -26,7 +24,7 @@ font-weight: 600; background-color: $back; transition: background-color box-shadow 0.3s ease-in; - color: mat.get-color-from-palette($primary, default-contrast); + color: map-get($primary, default-contrast); &.add { background-color: map-get($primary, 500); diff --git a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss index 0abe726237..c46ee15bc6 100644 --- a/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss +++ b/console/src/app/modules/app-radio/app-auth-method-radio/app-auth-method-radio.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - .auth-method-radio-button-wrapper { display: flex; flex-direction: row; @@ -20,7 +18,7 @@ @mixin app-auth-method-radio-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); $is-dark-theme: map-get($theme, is-dark); $background: map-get($theme, background); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); diff --git a/console/src/app/modules/app-radio/app-type-radio/app-type-radio.component.scss b/console/src/app/modules/app-radio/app-type-radio/app-type-radio.component.scss index df5d638331..10523c9f05 100644 --- a/console/src/app/modules/app-radio/app-type-radio/app-type-radio.component.scss +++ b/console/src/app/modules/app-radio/app-type-radio/app-type-radio.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - .app-type-radio-button-wrapper { display: flex; flex-direction: row; @@ -10,7 +8,7 @@ @mixin app-type-radio-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); $is-dark-theme: map-get($theme, is-dark); $background: map-get($theme, background); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); diff --git a/console/src/app/modules/avatar/avatar.component.scss b/console/src/app/modules/avatar/avatar.component.scss index 742728c5ba..ab454f998a 100644 --- a/console/src/app/modules/avatar/avatar.component.scss +++ b/console/src/app/modules/avatar/avatar.component.scss @@ -1,8 +1,6 @@ -@use '@angular/material' as mat; - @mixin avatar-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); .avatar-circle { border-radius: 50%; diff --git a/console/src/app/modules/card/card.module.ts b/console/src/app/modules/card/card.module.ts index 89702f05a6..2fc3c82b2d 100644 --- a/console/src/app/modules/card/card.module.ts +++ b/console/src/app/modules/card/card.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { CardComponent } from './card.component'; diff --git a/console/src/app/modules/card/card.scss b/console/src/app/modules/card/card.scss index fa39538ddd..89e7a324cb 100644 --- a/console/src/app/modules/card/card.scss +++ b/console/src/app/modules/card/card.scss @@ -1,10 +1,8 @@ -@use '@angular/material' as mat; - @mixin card-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); $background: map-get($theme, background); - $card-background-color: mat.get-color-from-palette($background, cards); + $card-background-color: map-get($background, cards); $is-dark-theme: map-get($theme, is-dark); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); $border-selected-color: if($is-dark-theme, #fff, #000); @@ -59,6 +57,13 @@ .cnsl-chip { height: auto; border: 1px solid $border-color; + background: if($is-dark-theme, map-get($background, state), #e4e7e4) !important; + + .cnsl-chip-content { + display: flex; + align-items: center; + font-size: 14px !important; + } } .cnsl-chip-list { diff --git a/console/src/app/modules/changes/changes.component.scss b/console/src/app/modules/changes/changes.component.scss index 59a25dfea7..438d27a5ad 100644 --- a/console/src/app/modules/changes/changes.component.scss +++ b/console/src/app/modules/changes/changes.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - .change-header { display: flex; justify-content: space-between; diff --git a/console/src/app/modules/changes/changes.module.ts b/console/src/app/modules/changes/changes.module.ts index 67ddc917f8..9c087e6383 100644 --- a/console/src/app/modules/changes/changes.module.ts +++ b/console/src/app/modules/changes/changes.module.ts @@ -1,10 +1,10 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { ScrollableModule } from 'src/app/directives/scrollable/scrollable.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; diff --git a/console/src/app/modules/client-keys/client-keys.component.html b/console/src/app/modules/client-keys/client-keys.component.html index 80c6d42682..8ac5869c8d 100644 --- a/console/src/app/modules/client-keys/client-keys.component.html +++ b/console/src/app/modules/client-keys/client-keys.component.html @@ -12,7 +12,9 @@ mat-raised-button (click)="openAddKey()" > - add{{ 'ACTIONS.NEW' | translate }} +
+ add{{ 'ACTIONS.NEW' | translate }} +
diff --git a/console/src/app/modules/client-keys/client-keys.component.ts b/console/src/app/modules/client-keys/client-keys.component.ts index b3c7aa0190..bfa32b0b54 100644 --- a/console/src/app/modules/client-keys/client-keys.component.ts +++ b/console/src/app/modules/client-keys/client-keys.component.ts @@ -1,7 +1,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; import { TranslateService } from '@ngx-translate/core'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Moment } from 'moment'; diff --git a/console/src/app/modules/client-keys/client-keys.module.ts b/console/src/app/modules/client-keys/client-keys.module.ts index 3b99a04243..6345df600e 100644 --- a/console/src/app/modules/client-keys/client-keys.module.ts +++ b/console/src/app/modules/client-keys/client-keys.module.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/contributors/contributors.component.scss b/console/src/app/modules/contributors/contributors.component.scss index 2782272969..1a15257edb 100644 --- a/console/src/app/modules/contributors/contributors.component.scss +++ b/console/src/app/modules/contributors/contributors.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin contributors-theme($theme) { $foreground: map-get($theme, foreground); $background: map-get($theme, background); diff --git a/console/src/app/modules/contributors/contributors.module.ts b/console/src/app/modules/contributors/contributors.module.ts index fbd3de6044..3738b34ac0 100644 --- a/console/src/app/modules/contributors/contributors.module.ts +++ b/console/src/app/modules/contributors/contributors.module.ts @@ -1,9 +1,9 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { RoleTransformPipeModule } from 'src/app/pipes/role-transform/role-transform.module'; diff --git a/console/src/app/modules/create-layout/create-layout.component.scss b/console/src/app/modules/create-layout/create-layout.component.scss index 172d993cb8..3cbbebb823 100644 --- a/console/src/app/modules/create-layout/create-layout.component.scss +++ b/console/src/app/modules/create-layout/create-layout.component.scss @@ -1,8 +1,6 @@ -@use '@angular/material' as mat; - @mixin app-create-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); // Number of steps creating app $steps: 3; @@ -35,7 +33,11 @@ .abort { font-size: 1.2rem; - margin-left: 2rem; + margin-left: 1.5rem; + text-transform: uppercase; + font-size: 14px; + opacity: 0.8; + letter-spacing: 0.05em; } .abort-2 { diff --git a/console/src/app/modules/create-layout/create-layout.module.ts b/console/src/app/modules/create-layout/create-layout.module.ts index 03e1fd412d..346ae71576 100644 --- a/console/src/app/modules/create-layout/create-layout.module.ts +++ b/console/src/app/modules/create-layout/create-layout.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CreateLayoutComponent } from './create-layout.component'; diff --git a/console/src/app/modules/detail-layout/detail-layout.component.scss b/console/src/app/modules/detail-layout/detail-layout.component.scss index 7e5628314d..b20cc2197a 100644 --- a/console/src/app/modules/detail-layout/detail-layout.component.scss +++ b/console/src/app/modules/detail-layout/detail-layout.component.scss @@ -1,10 +1,8 @@ -@use '@angular/material' as mat; - @mixin detail-layout-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); $is-dark-theme: map-get($theme, is-dark); - $lighter-color: rgba(mat.get-color-from-palette($primary, 300), 0.5); + $lighter-color: rgba(map-get($primary, 300), 0.5); .detail-layout-head { margin-bottom: 2rem; diff --git a/console/src/app/modules/detail-layout/detail-layout.module.ts b/console/src/app/modules/detail-layout/detail-layout.module.ts index ae458d3261..bfb549a231 100644 --- a/console/src/app/modules/detail-layout/detail-layout.module.ts +++ b/console/src/app/modules/detail-layout/detail-layout.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { RouterModule } from '@angular/router'; import { BackModule } from 'src/app/directives/back/back.module'; diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.component.ts b/console/src/app/modules/display-json-dialog/display-json-dialog.component.ts index 1e0a781294..764f74a9b0 100644 --- a/console/src/app/modules/display-json-dialog/display-json-dialog.component.ts +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { mapTo } from 'rxjs'; import { Event } from 'src/app/proto/generated/zitadel/event_pb'; diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.module.ts b/console/src/app/modules/display-json-dialog/display-json-dialog.module.ts index 065f3810a9..369ca2e043 100644 --- a/console/src/app/modules/display-json-dialog/display-json-dialog.module.ts +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.module.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { TranslateModule } from '@ngx-translate/core'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { FormsModule } from '@angular/forms'; +import { MatDialogModule } from '@angular/material/dialog'; import { CodemirrorModule } from '@ctrl/ngx-codemirror'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; import { ToObjectPipeModule } from 'src/app/pipes/to-object/to-object.module'; @@ -19,6 +20,7 @@ import { DisplayJsonDialogComponent } from './display-json-dialog.component'; FormsModule, TranslateModule, MatButtonModule, + MatDialogModule, MatIconModule, CodemirrorModule, TimestampToDatePipeModule, diff --git a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.html b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.html index 5dd2f7896c..167185c460 100644 --- a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.html +++ b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.html @@ -1,4 +1,4 @@ -{{ 'ORG.DOMAINS.ADD.TITLE' | translate }} +

{{ 'ORG.DOMAINS.ADD.TITLE' | translate }}

{{ 'ORG.DOMAINS.ADD.DESCRIPTION' | translate }}

diff --git a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.scss b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.scss index d865b143c0..34245bc39f 100644 --- a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.scss +++ b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.scss @@ -7,10 +7,6 @@ font-size: 0.9rem; } -.form-field { - width: 100%; -} - .action { display: flex; justify-content: space-between; diff --git a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.ts b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.ts index 9adce8dc15..0bd2c3d5ae 100644 --- a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.ts +++ b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @Component({ selector: 'cnsl-add-domain-dialog', diff --git a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.module.ts b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.module.ts index 986ad68679..b6c2ac7323 100644 --- a/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.module.ts +++ b/console/src/app/modules/domains/add-domain-dialog/add-domain-dialog.module.ts @@ -1,14 +1,15 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatButtonModule } from '@angular/material/button'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; +import { MatDialogModule } from '@angular/material/dialog'; import { AddDomainDialogComponent } from './add-domain-dialog.component'; @NgModule({ declarations: [AddDomainDialogComponent], - imports: [CommonModule, TranslateModule, MatButtonModule, InputModule, FormsModule], + imports: [CommonModule, TranslateModule, MatDialogModule, MatButtonModule, InputModule, FormsModule], }) export class AddDomainDialogModule {} diff --git a/console/src/app/modules/domains/domain-verification/domain-verification.component.ts b/console/src/app/modules/domains/domain-verification/domain-verification.component.ts index 46222cc3ca..94a79908da 100644 --- a/console/src/app/modules/domains/domain-verification/domain-verification.component.ts +++ b/console/src/app/modules/domains/domain-verification/domain-verification.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { saveAs } from 'file-saver'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; import { GenerateOrgDomainValidationResponse } from 'src/app/proto/generated/zitadel/management_pb'; diff --git a/console/src/app/modules/domains/domains.component.html b/console/src/app/modules/domains/domains.component.html index b44caeca9f..1841e30a03 100644 --- a/console/src/app/modules/domains/domains.component.html +++ b/console/src/app/modules/domains/domains.component.html @@ -9,7 +9,7 @@ rel="noreferrer" target="_blank" > - + info_outline

{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}

@@ -24,9 +24,11 @@ class="cnsl-action-button" (click)="addNewDomain()" > - add - {{ 'ACTIONS.NEW' | translate }} - +
+ add + {{ 'ACTIONS.NEW' | translate }} + +
diff --git a/console/src/app/modules/domains/domains.component.scss b/console/src/app/modules/domains/domains.component.scss index 8a324173b5..32455d7929 100644 --- a/console/src/app/modules/domains/domains.component.scss +++ b/console/src/app/modules/domains/domains.component.scss @@ -10,7 +10,7 @@ margin: 0; } - a i { + a .icon { font-size: 1.2rem; height: 1.2rem; line-height: 1.2rem; diff --git a/console/src/app/modules/domains/domains.component.ts b/console/src/app/modules/domains/domains.component.ts index 6a957e06c9..4c57f18c60 100644 --- a/console/src/app/modules/domains/domains.component.ts +++ b/console/src/app/modules/domains/domains.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatDialog } from '@angular/material/dialog'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { Domain, DomainValidationType } from 'src/app/proto/generated/zitadel/org_pb'; @@ -63,7 +63,6 @@ export class DomainsComponent implements OnInit { public addNewDomain(): void { const dialogRef = this.dialog.open(AddDomainDialogComponent, { - data: {}, width: '400px', }); diff --git a/console/src/app/modules/domains/domains.module.ts b/console/src/app/modules/domains/domains.module.ts index 15fb0c0605..03bb40c94c 100644 --- a/console/src/app/modules/domains/domains.module.ts +++ b/console/src/app/modules/domains/domains.module.ts @@ -1,9 +1,9 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module'; diff --git a/console/src/app/modules/edit-text/edit-text.component.scss b/console/src/app/modules/edit-text/edit-text.component.scss index 402a559fad..d19b42693a 100644 --- a/console/src/app/modules/edit-text/edit-text.component.scss +++ b/console/src/app/modules/edit-text/edit-text.component.scss @@ -3,10 +3,10 @@ $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); @@ -41,7 +41,7 @@ padding: 4px 0.5rem; font-size: 12px; background: $primary-color; - color: mat.get-color-from-palette($primary, default-contrast); + color: map-get($primary, default-contrast); margin: 0.25rem; display: flex; align-items: center; @@ -98,7 +98,7 @@ display: flex; flex-direction: column; align-self: flex-start; - margin-top: 30px; + margin-top: 20px; } } } diff --git a/console/src/app/modules/edit-text/edit-text.module.ts b/console/src/app/modules/edit-text/edit-text.module.ts index 68e66d5905..6c80a7dfa4 100644 --- a/console/src/app/modules/edit-text/edit-text.module.ts +++ b/console/src/app/modules/edit-text/edit-text.module.ts @@ -2,9 +2,9 @@ import { TextFieldModule } from '@angular/cdk/text-field'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { InputModule } from 'src/app/modules/input/input.module'; diff --git a/console/src/app/modules/filter-events/filter-events.component.html b/console/src/app/modules/filter-events/filter-events.component.html index 7e76d422e6..5d835c32b5 100644 --- a/console/src/app/modules/filter-events/filter-events.component.html +++ b/console/src/app/modules/filter-events/filter-events.component.html @@ -3,15 +3,20 @@ mat-stroked-button cdkOverlayOrigin (click)="showFilter = !showFilter" - class="cnsl-action-button" #triggereventfilter="cdkOverlayOrigin" data-e2e="open-filter-button" > - - {{ 'ACTIONS.FILTER' | translate }} - {{ queryCount }} - - +
+ + {{ 'ACTIONS.FILTER' | translate }} + {{ queryCount }} + + +
- - {{ 'MENU.INSTANCE' | translate }} - +
+ {{ 'MENU.INSTANCE' | translate }} + +
- {{ 'MENU.ORGANIZATION' | translate }} - +
+ {{ 'MENU.ORGANIZATION' | translate }} + +
diff --git a/console/src/app/modules/header/header.component.scss b/console/src/app/modules/header/header.component.scss index b4ff0928c6..2b219cc0fa 100644 --- a/console/src/app/modules/header/header.component.scss +++ b/console/src/app/modules/header/header.component.scss @@ -9,17 +9,17 @@ $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); .filter-form { margin: 0 0.5rem; - color: mat.get-color-from-palette($foreground, text) !important; + color: map-get($foreground, text) !important; } .header-wrapper { diff --git a/console/src/app/modules/header/header.module.ts b/console/src/app/modules/header/header.module.ts index 9342548ac9..bc887c9ebd 100644 --- a/console/src/app/modules/header/header.module.ts +++ b/console/src/app/modules/header/header.module.ts @@ -2,10 +2,10 @@ import { OverlayModule } from '@angular/cdk/overlay'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { MatRippleModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatToolbarModule } from '@angular/material/toolbar'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; diff --git a/console/src/app/modules/idp-table/idp-table.component.scss b/console/src/app/modules/idp-table/idp-table.component.scss index 29cb7485a4..8ed40c9efe 100644 --- a/console/src/app/modules/idp-table/idp-table.component.scss +++ b/console/src/app/modules/idp-table/idp-table.component.scss @@ -3,10 +3,10 @@ $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); diff --git a/console/src/app/modules/idp-table/idp-table.component.ts b/console/src/app/modules/idp-table/idp-table.component.ts index b871d9329d..c28b4dd49a 100644 --- a/console/src/app/modules/idp-table/idp-table.component.ts +++ b/console/src/app/modules/idp-table/idp-table.component.ts @@ -1,7 +1,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; import { Router, RouterLink } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; diff --git a/console/src/app/modules/idp-table/idp-table.module.ts b/console/src/app/modules/idp-table/idp-table.module.ts index 78251c7402..c428fcccca 100644 --- a/console/src/app/modules/idp-table/idp-table.module.ts +++ b/console/src/app/modules/idp-table/idp-table.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/info-dialog/info-dialog.component.ts b/console/src/app/modules/info-dialog/info-dialog.component.ts index 408be61197..c85abff9ad 100644 --- a/console/src/app/modules/info-dialog/info-dialog.component.ts +++ b/console/src/app/modules/info-dialog/info-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { InfoSectionType } from '../info-section/info-section.component'; diff --git a/console/src/app/modules/info-dialog/info-dialog.module.ts b/console/src/app/modules/info-dialog/info-dialog.module.ts index 5bc1712ba3..573111b05a 100644 --- a/console/src/app/modules/info-dialog/info-dialog.module.ts +++ b/console/src/app/modules/info-dialog/info-dialog.module.ts @@ -1,15 +1,16 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatButtonModule } from '@angular/material/button'; import { TranslateModule } from '@ngx-translate/core'; +import { MatDialogModule } from '@angular/material/dialog'; import { InfoSectionModule } from '../info-section/info-section.module'; import { InputModule } from '../input/input.module'; import { InfoDialogComponent } from './info-dialog.component'; @NgModule({ declarations: [InfoDialogComponent], - imports: [CommonModule, FormsModule, TranslateModule, InfoSectionModule, MatButtonModule, InputModule], + imports: [CommonModule, FormsModule, MatDialogModule, TranslateModule, InfoSectionModule, MatButtonModule, InputModule], }) export class InfoDialogModule {} diff --git a/console/src/app/modules/info-overlay/info-overlay.component.scss b/console/src/app/modules/info-overlay/info-overlay.component.scss index 7bd92caaee..30c7c73290 100644 --- a/console/src/app/modules/info-overlay/info-overlay.component.scss +++ b/console/src/app/modules/info-overlay/info-overlay.component.scss @@ -3,10 +3,10 @@ $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); diff --git a/console/src/app/modules/info-overlay/info-overlay.module.ts b/console/src/app/modules/info-overlay/info-overlay.module.ts index c1e5271646..5dec52960f 100644 --- a/console/src/app/modules/info-overlay/info-overlay.module.ts +++ b/console/src/app/modules/info-overlay/info-overlay.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { TranslateModule } from '@ngx-translate/core'; import { InfoOverlayComponent } from './info-overlay.component'; diff --git a/console/src/app/modules/info-row/info-row.component.scss b/console/src/app/modules/info-row/info-row.component.scss index 71f602c322..4df6179f7c 100644 --- a/console/src/app/modules/info-row/info-row.component.scss +++ b/console/src/app/modules/info-row/info-row.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin info-row-theme($theme) { $foreground: map-get($theme, foreground); $button-text-color: map-get($foreground, text); diff --git a/console/src/app/modules/info-row/info-row.module.ts b/console/src/app/modules/info-row/info-row.module.ts index 3faec3efda..4ee88f86d7 100644 --- a/console/src/app/modules/info-row/info-row.module.ts +++ b/console/src/app/modules/info-row/info-row.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; diff --git a/console/src/app/modules/info-section/info-section.component.scss b/console/src/app/modules/info-section/info-section.component.scss index ee49563203..8399226662 100644 --- a/console/src/app/modules/info-section/info-section.component.scss +++ b/console/src/app/modules/info-section/info-section.component.scss @@ -1,10 +1,8 @@ -@use '@angular/material' as mat; - @mixin info-section-theme($theme) { $primary: map-get($theme, primary); $background: map-get($theme, background); $foreground: map-get($theme, foreground); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); $is-dark-theme: map-get($theme, is-dark); .info-section-row { diff --git a/console/src/app/modules/input/input.directive.ts b/console/src/app/modules/input/input.directive.ts index bd8e6b7c8e..ca160b680d 100644 --- a/console/src/app/modules/input/input.directive.ts +++ b/console/src/app/modules/input/input.directive.ts @@ -17,15 +17,8 @@ import { } from '@angular/core'; import { FormGroupDirective, NgControl, NgForm } from '@angular/forms'; import { CanUpdateErrorState, ErrorStateMatcher, mixinErrorState } from '@angular/material/core'; -import { - MatLegacyFormField as MatFormField, - MatLegacyFormFieldControl as MatFormFieldControl, - MAT_LEGACY_FORM_FIELD as MAT_FORM_FIELD, -} from '@angular/material/legacy-form-field'; -import { - getMatLegacyInputUnsupportedTypeError as getMatInputUnsupportedTypeError, - MAT_LEGACY_INPUT_VALUE_ACCESSOR as MAT_INPUT_VALUE_ACCESSOR, -} from '@angular/material/legacy-input'; +import { MatFormField, MatFormFieldControl, MAT_FORM_FIELD } from '@angular/material/form-field'; +import { getMatInputUnsupportedTypeError, MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input'; import { Subject } from 'rxjs'; // Invalid input type. Using one of these will throw an MatInputUnsupportedTypeError. @@ -370,7 +363,7 @@ export class InputDirective private _dirtyCheckPlaceholder(): void { // If we're hiding the native placeholder, it should also be cleared from the DOM, otherwise // screen readers will read it out twice: once from the label and once from the attribute. - const placeholder = this._formField?._hideControlPlaceholder?.() ? null : this.placeholder; + const placeholder = null; if (placeholder !== this._previousPlaceholder) { const element = this._elementRef.nativeElement; this._previousPlaceholder = placeholder; diff --git a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.html b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.html index 013a02a404..c3a755e96d 100644 --- a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.html +++ b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.html @@ -34,7 +34,7 @@
- diff --git a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.scss b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.scss index d389100871..3d901287dd 100644 --- a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.scss +++ b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin keyboard-shortcuts-theme($theme) { $primary: map-get($theme, primary); $background: map-get($theme, background); @@ -7,9 +5,9 @@ $text-color: map-get($foreground, text); $accent: map-get($theme, accent); $is-dark-theme: map-get($theme, is-dark); - $accent-color: mat.get-color-from-palette($primary, 500); + $accent-color: map-get($primary, 500); $back: map-get($background, background); - $card-background-color: mat.get-color-from-palette($background, cards); + $card-background-color: map-get($background, cards); .keyboard-shortcuts-group { $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); diff --git a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.ts b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.ts index 2eda8be046..15cb81f87d 100644 --- a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.ts +++ b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { KeyboardShortcut, ORGSHORTCUTS, SIDEWIDESHORTCUTS } from 'src/app/services/keyboard-shortcuts/keyboard-shortcuts'; diff --git a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.module.ts b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.module.ts index e8a31e1c46..3d725cc6cb 100644 --- a/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.module.ts +++ b/console/src/app/modules/keyboard-shortcuts/keyboard-shortcuts.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/label/label.component.scss b/console/src/app/modules/label/label.component.scss index f4046d4866..c116b38f38 100644 --- a/console/src/app/modules/label/label.component.scss +++ b/console/src/app/modules/label/label.component.scss @@ -1,11 +1,9 @@ -@use '@angular/material' as mat; - @mixin cnsl-label-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); $is-dark-theme: map-get($theme, is-dark); $warn: map-get($theme, warn); - $warn-color: mat.get-color-from-palette($warn, 500); + $warn-color: map-get($warn, 500); $foreground: map-get($theme, foreground); $secondary-text: map-get($foreground, secondary-text); diff --git a/console/src/app/modules/machine-keys/machine-keys.component.html b/console/src/app/modules/machine-keys/machine-keys.component.html index 49cd48855a..9e24fb2b9e 100644 --- a/console/src/app/modules/machine-keys/machine-keys.component.html +++ b/console/src/app/modules/machine-keys/machine-keys.component.html @@ -12,7 +12,9 @@ mat-raised-button (click)="openAddKey()" > - add{{ 'ACTIONS.NEW' | translate }} +
+ add{{ 'ACTIONS.NEW' | translate }} +
diff --git a/console/src/app/modules/machine-keys/machine-keys.component.ts b/console/src/app/modules/machine-keys/machine-keys.component.ts index a12e6bcc31..06a945062d 100644 --- a/console/src/app/modules/machine-keys/machine-keys.component.ts +++ b/console/src/app/modules/machine-keys/machine-keys.component.ts @@ -1,7 +1,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; import { TranslateService } from '@ngx-translate/core'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Moment } from 'moment'; diff --git a/console/src/app/modules/machine-keys/machine-keys.module.ts b/console/src/app/modules/machine-keys/machine-keys.module.ts index e94b999446..397eae8bfd 100644 --- a/console/src/app/modules/machine-keys/machine-keys.module.ts +++ b/console/src/app/modules/machine-keys/machine-keys.module.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/members-table/members-table.component.html b/console/src/app/modules/members-table/members-table.component.html index 7f69806bc5..3b2e70cff7 100644 --- a/console/src/app/modules/members-table/members-table.component.html +++ b/console/src/app/modules/members-table/members-table.component.html @@ -17,40 +17,44 @@
- - @@ -108,22 +112,24 @@ diff --git a/console/src/app/modules/members-table/members-table.component.spec.ts b/console/src/app/modules/members-table/members-table.component.spec.ts index 99cda41fe7..f20807e7a6 100644 --- a/console/src/app/modules/members-table/members-table.component.spec.ts +++ b/console/src/app/modules/members-table/members-table.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MembersTableComponent } from './members-table.component'; diff --git a/console/src/app/modules/members-table/members-table.component.ts b/console/src/app/modules/members-table/members-table.component.ts index 69933abf9f..3ac361488d 100644 --- a/console/src/app/modules/members-table/members-table.component.ts +++ b/console/src/app/modules/members-table/members-table.component.ts @@ -1,7 +1,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyTable as MatTable } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTable } from '@angular/material/table'; import { Observable, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { InstanceMembersDataSource } from 'src/app/pages/instance/instance-members/instance-members-datasource'; diff --git a/console/src/app/modules/members-table/members-table.module.ts b/console/src/app/modules/members-table/members-table.module.ts index 3ae53d96bc..50c5884225 100644 --- a/console/src/app/modules/members-table/members-table.module.ts +++ b/console/src/app/modules/members-table/members-table.module.ts @@ -1,14 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatSelectModule } from '@angular/material/select'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; diff --git a/console/src/app/modules/memberships-table/memberships-table.component.html b/console/src/app/modules/memberships-table/memberships-table.component.html index 94a24003aa..4465ed3d21 100644 --- a/console/src/app/modules/memberships-table/memberships-table.component.html +++ b/console/src/app/modules/memberships-table/memberships-table.component.html @@ -18,25 +18,29 @@
- - - - - +
+ - - - - + +
+ +
+
+ + - - + + + + + + +
{{ 'ROLESLABEL' | translate }} - - + -
- {{ role | roletransform }} - -
-
+
+
+ {{ role | roletransform }} + +
+ +
- - @@ -65,28 +69,30 @@ diff --git a/console/src/app/modules/memberships-table/memberships-table.component.spec.ts b/console/src/app/modules/memberships-table/memberships-table.component.spec.ts index 24dfcb1fe9..2c2550b752 100644 --- a/console/src/app/modules/memberships-table/memberships-table.component.spec.ts +++ b/console/src/app/modules/memberships-table/memberships-table.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MembershipsTableComponent } from './memberships-table.component'; diff --git a/console/src/app/modules/memberships-table/memberships-table.component.ts b/console/src/app/modules/memberships-table/memberships-table.component.ts index e7a2ed26b7..c7771d2abb 100644 --- a/console/src/app/modules/memberships-table/memberships-table.component.ts +++ b/console/src/app/modules/memberships-table/memberships-table.component.ts @@ -1,6 +1,6 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; -import { MatLegacyTable as MatTable } from '@angular/material/legacy-table'; +import { MatTable } from '@angular/material/table'; import { Router } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; diff --git a/console/src/app/modules/memberships-table/memberships-table.module.ts b/console/src/app/modules/memberships-table/memberships-table.module.ts index d0fb06e83c..8ab03ad3e9 100644 --- a/console/src/app/modules/memberships-table/memberships-table.module.ts +++ b/console/src/app/modules/memberships-table/memberships-table.module.ts @@ -1,15 +1,15 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; diff --git a/console/src/app/modules/meta-layout/meta-layout.module.ts b/console/src/app/modules/meta-layout/meta-layout.module.ts index c4f9c84d0d..a988dd587a 100644 --- a/console/src/app/modules/meta-layout/meta-layout.module.ts +++ b/console/src/app/modules/meta-layout/meta-layout.module.ts @@ -1,7 +1,7 @@ import { LayoutModule } from '@angular/cdk/layout'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatButtonModule } from '@angular/material/button'; import { MetaLayoutComponent } from './meta-layout.component'; diff --git a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.html b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.html index 5c04eb96d2..31262d9cf3 100644 --- a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.html +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.html @@ -1,10 +1,8 @@
-

{{ 'METADATA.TITLE' | translate }}

+

{{ 'METADATA.TITLE' | translate }}

{{ ts | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}

-
-

{{ 'METADATA.DESCRIPTION' | translate }}

diff --git a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts index b6adf3c1e9..599e58d937 100644 --- a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; import { ToastService } from 'src/app/services/toast.service'; @@ -14,7 +11,6 @@ import { ToastService } from 'src/app/services/toast.service'; }) export class MetadataDialogComponent { public metadata: Partial[] = []; - public loading: boolean = false; public ts!: Timestamp.AsObject | undefined; constructor( diff --git a/console/src/app/modules/metadata/metadata.module.ts b/console/src/app/modules/metadata/metadata.module.ts index e23753df27..dd87968efd 100644 --- a/console/src/app/modules/metadata/metadata.module.ts +++ b/console/src/app/modules/metadata/metadata.module.ts @@ -1,17 +1,17 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; import { CardModule } from '../card/card.module'; -import { MatLegacyTableModule } from '@angular/material/legacy-table'; +import { MatTableModule } from '@angular/material/table'; import { InputModule } from '../input/input.module'; import { RefreshTableModule } from '../refresh-table/refresh-table.module'; import { MetadataDialogComponent } from './metadata-dialog/metadata-dialog.component'; @@ -33,7 +33,7 @@ import { MetadataComponent } from './metadata/metadata.component'; LocalizedDatePipeModule, TimestampToDatePipeModule, RefreshTableModule, - MatLegacyTableModule, + MatTableModule, ], exports: [MetadataComponent, MetadataDialogComponent], }) diff --git a/console/src/app/modules/metadata/metadata/metadata.component.ts b/console/src/app/modules/metadata/metadata/metadata.component.ts index 22ff727a8a..96c454f7c3 100644 --- a/console/src/app/modules/metadata/metadata/metadata.component.ts +++ b/console/src/app/modules/metadata/metadata/metadata.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core'; -import { MatLegacyTable as MatTable, MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; import { MatSort } from '@angular/material/sort'; +import { MatTable, MatTableDataSource } from '@angular/material/table'; import { BehaviorSubject, Observable } from 'rxjs'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; diff --git a/console/src/app/modules/name-dialog/name-dialog.component.html b/console/src/app/modules/name-dialog/name-dialog.component.html index fe1f6782ea..e57c70269c 100644 --- a/console/src/app/modules/name-dialog/name-dialog.component.html +++ b/console/src/app/modules/name-dialog/name-dialog.component.html @@ -1,15 +1,16 @@

{{ data.titleKey | translate }} {{ data?.number }}

-

{{ data.descKey | translate }}

+

{{ data.descKey | translate }}

+ {{ data.labelKey | translate }}
- diff --git a/console/src/app/modules/name-dialog/name-dialog.component.ts b/console/src/app/modules/name-dialog/name-dialog.component.ts index 7baa65b2a7..a3e450b98d 100644 --- a/console/src/app/modules/name-dialog/name-dialog.component.ts +++ b/console/src/app/modules/name-dialog/name-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @Component({ selector: 'cnsl-name-dialog', diff --git a/console/src/app/modules/name-dialog/name-dialog.module.ts b/console/src/app/modules/name-dialog/name-dialog.module.ts index 802ab1448b..dfcc85e378 100644 --- a/console/src/app/modules/name-dialog/name-dialog.module.ts +++ b/console/src/app/modules/name-dialog/name-dialog.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from '../input/input.module'; diff --git a/console/src/app/modules/nav-toggle/nav-toggle.component.scss b/console/src/app/modules/nav-toggle/nav-toggle.component.scss index a04b13d0d6..d69e6c4974 100644 --- a/console/src/app/modules/nav-toggle/nav-toggle.component.scss +++ b/console/src/app/modules/nav-toggle/nav-toggle.component.scss @@ -1,14 +1,12 @@ -@use '@angular/material' as mat; - @mixin nav-toggle-theme($theme) { $primary: map-get($theme, primary); $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); @@ -19,7 +17,7 @@ font-size: 14px; line-height: 14px; padding: 0.4rem 12px; - color: mat.get-color-from-palette($foreground, text) !important; + color: map-get($foreground, text); transition: all 0.2s ease; text-decoration: none; border-radius: 50vw; @@ -76,7 +74,7 @@ &.active { background-color: $primary-color; - color: mat.get-color-from-palette($foreground, toolbar-items) !important; + color: map-get($primary, default-contrast); .c_label { .count { diff --git a/console/src/app/modules/nav/nav.component.html b/console/src/app/modules/nav/nav.component.html index cbcdcecb46..3317e41f91 100644 --- a/console/src/app/modules/nav/nav.component.html +++ b/console/src/app/modules/nav/nav.component.html @@ -204,19 +204,22 @@ - +
+ +
+
diff --git a/console/src/app/modules/org-context/org-context.component.scss b/console/src/app/modules/org-context/org-context.component.scss index f20910c5bb..d5736cba2a 100644 --- a/console/src/app/modules/org-context/org-context.component.scss +++ b/console/src/app/modules/org-context/org-context.component.scss @@ -3,10 +3,10 @@ $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); @@ -41,7 +41,7 @@ .show-all { width: 100%; - color: mat.get-color-from-palette($foreground, text); + color: map-get($foreground, text); border-top: 1px solid map-get($foreground, divider); } @@ -59,6 +59,10 @@ position: relative; overflow: hidden; + .mdc-button__label { + width: 100%; + } + .org-flex-row { display: flex; align-items: center; @@ -77,6 +81,7 @@ margin-right: -0.5rem; visibility: hidden; opacity: 0.5; + padding: 0; &:hover { opacity: 1; @@ -85,7 +90,7 @@ mat-icon { font-size: 20px; height: 20px; - width: 20px; + width: 36px; } } } diff --git a/console/src/app/modules/org-context/org-context.module.ts b/console/src/app/modules/org-context/org-context.module.ts index beb12daf2a..2d025f7178 100644 --- a/console/src/app/modules/org-context/org-context.module.ts +++ b/console/src/app/modules/org-context/org-context.module.ts @@ -2,10 +2,10 @@ import { A11yModule } from '@angular/cdk/a11y'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/org-table/org-table.component.html b/console/src/app/modules/org-table/org-table.component.html index 1a28850227..d0582c87b4 100644 --- a/console/src/app/modules/org-table/org-table.component.html +++ b/console/src/app/modules/org-table/org-table.component.html @@ -8,10 +8,12 @@ - - add - {{ 'ACTIONS.NEW' | translate }} - + +
+ add + {{ 'ACTIONS.NEW' | translate }} + +
@@ -31,20 +33,21 @@ diff --git a/console/src/app/modules/org-table/org-table.component.scss b/console/src/app/modules/org-table/org-table.component.scss index cc165bacc9..4a7ad537df 100644 --- a/console/src/app/modules/org-table/org-table.component.scss +++ b/console/src/app/modules/org-table/org-table.component.scss @@ -1,23 +1,28 @@ td { cursor: pointer; - .cpy-button { - visibility: hidden; + .primary-domain-wrapper { + display: flex; + align-items: center; - i { - pointer-events: none; + .cpy-button { + visibility: hidden; + + i { + pointer-events: none; + } + } + + &:hover { + .cpy-button { + visibility: visible; + } } } .orgdefaultlabel { margin-left: 0.5rem; } - - &:hover { - .cpy-button { - visibility: visible; - } - } } .pointer { diff --git a/console/src/app/modules/org-table/org-table.component.ts b/console/src/app/modules/org-table/org-table.component.ts index b60c049fd5..bc2fcc71aa 100644 --- a/console/src/app/modules/org-table/org-table.component.ts +++ b/console/src/app/modules/org-table/org-table.component.ts @@ -1,7 +1,7 @@ import { LiveAnnouncer } from '@angular/cdk/a11y'; import { Component, Input, ViewChild } from '@angular/core'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; import { MatSort, Sort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; diff --git a/console/src/app/modules/org-table/org-table.module.ts b/console/src/app/modules/org-table/org-table.module.ts index 9a1f1b6b3c..b4650e381c 100644 --- a/console/src/app/modules/org-table/org-table.module.ts +++ b/console/src/app/modules/org-table/org-table.module.ts @@ -1,12 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatRadioModule } from '@angular/material/radio'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; diff --git a/console/src/app/modules/paginator/paginator.component.scss b/console/src/app/modules/paginator/paginator.component.scss index 66e04072af..b280d4b0ab 100644 --- a/console/src/app/modules/paginator/paginator.component.scss +++ b/console/src/app/modules/paginator/paginator.component.scss @@ -49,8 +49,8 @@ } } -::ng-deep .paginator-select.mat-select { +::ng-deep .paginator-select.mat-mdc-select { min-width: 60px; height: 36px !important; - padding-top: 8px !important; + padding-top: 4px !important; } diff --git a/console/src/app/modules/paginator/paginator.module.ts b/console/src/app/modules/paginator/paginator.module.ts index 663c4c9e0d..c2926e1f75 100644 --- a/console/src/app/modules/paginator/paginator.module.ts +++ b/console/src/app/modules/paginator/paginator.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSelectModule } from '@angular/material/select'; import { TranslateModule } from '@ngx-translate/core'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.module.ts b/console/src/app/modules/password-complexity-view/password-complexity-view.module.ts index 8a27c5e394..8d2829677c 100644 --- a/console/src/app/modules/password-complexity-view/password-complexity-view.module.ts +++ b/console/src/app/modules/password-complexity-view/password-complexity-view.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { TranslateModule } from '@ngx-translate/core'; import { PasswordComplexityViewComponent } from './password-complexity-view.component'; diff --git a/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html b/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html index c1df8a5cd5..3d2e60adea 100644 --- a/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html +++ b/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.html @@ -12,7 +12,9 @@ mat-raised-button (click)="openAddKey()" > - add{{ 'ACTIONS.NEW' | translate }} +
+ add{{ 'ACTIONS.NEW' | translate }} +
diff --git a/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.ts b/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.ts index 7d77fb0bff..17a9e980e5 100644 --- a/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.ts +++ b/console/src/app/modules/personal-access-tokens/personal-access-tokens.component.ts @@ -1,7 +1,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; import { TranslateService } from '@ngx-translate/core'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Moment } from 'moment'; diff --git a/console/src/app/modules/personal-access-tokens/personal-access-tokens.module.ts b/console/src/app/modules/personal-access-tokens/personal-access-tokens.module.ts index 530c5d9941..b4b7b4942c 100644 --- a/console/src/app/modules/personal-access-tokens/personal-access-tokens.module.ts +++ b/console/src/app/modules/personal-access-tokens/personal-access-tokens.module.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/policies/domain-policy/domain-policy.component.ts b/console/src/app/modules/policies/domain-policy/domain-policy.component.ts index fb8efe1ff6..0ac32cd0fb 100644 --- a/console/src/app/modules/policies/domain-policy/domain-policy.component.ts +++ b/console/src/app/modules/policies/domain-policy/domain-policy.component.ts @@ -1,5 +1,5 @@ import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatDialog } from '@angular/material/dialog'; import { Subscription } from 'rxjs'; import { AddCustomDomainPolicyRequest, diff --git a/console/src/app/modules/policies/domain-policy/domain-policy.module.ts b/console/src/app/modules/policies/domain-policy/domain-policy.module.ts index e152220a40..e7f8174492 100644 --- a/console/src/app/modules/policies/domain-policy/domain-policy.module.ts +++ b/console/src/app/modules/policies/domain-policy/domain-policy.module.ts @@ -1,12 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module'; diff --git a/console/src/app/modules/policies/general-settings/general-settings.module.ts b/console/src/app/modules/policies/general-settings/general-settings.module.ts index 93c095db76..98f0e4cb53 100644 --- a/console/src/app/modules/policies/general-settings/general-settings.module.ts +++ b/console/src/app/modules/policies/general-settings/general-settings.module.ts @@ -1,9 +1,9 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; import { TranslateModule } from '@ngx-translate/core'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; diff --git a/console/src/app/modules/policies/idp-settings/idp-settings.component.scss b/console/src/app/modules/policies/idp-settings/idp-settings.component.scss index d092423b95..095a05491e 100644 --- a/console/src/app/modules/policies/idp-settings/idp-settings.component.scss +++ b/console/src/app/modules/policies/idp-settings/idp-settings.component.scss @@ -1,8 +1,6 @@ -@use '@angular/material' as mat; - @mixin idp-settings-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); $is-dark-theme: map-get($theme, is-dark); $background: map-get($theme, background); $foreground: map-get($theme, foreground); diff --git a/console/src/app/modules/policies/idp-settings/idp-settings.module.ts b/console/src/app/modules/policies/idp-settings/idp-settings.module.ts index 79264fbe3c..33730519ad 100644 --- a/console/src/app/modules/policies/idp-settings/idp-settings.module.ts +++ b/console/src/app/modules/policies/idp-settings/idp-settings.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; @@ -15,7 +15,7 @@ import { IdpSettingsComponent } from './idp-settings.component'; declarations: [IdpSettingsComponent], imports: [ CommonModule, - MatLegacyButtonModule, + MatButtonModule, CardModule, MatIconModule, IdpTableModule, diff --git a/console/src/app/modules/policies/login-policy/factor-table/dialog-add-type/dialog-add-type.component.ts b/console/src/app/modules/policies/login-policy/factor-table/dialog-add-type/dialog-add-type.component.ts index 5a71691ed0..7314e41933 100644 --- a/console/src/app/modules/policies/login-policy/factor-table/dialog-add-type/dialog-add-type.component.ts +++ b/console/src/app/modules/policies/login-policy/factor-table/dialog-add-type/dialog-add-type.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MultiFactorType, SecondFactorType } from 'src/app/proto/generated/zitadel/policy_pb'; enum LoginMethodComponentType { diff --git a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.html b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.html index 948f5cfa26..e31877be75 100644 --- a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.html +++ b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.html @@ -21,12 +21,14 @@
diff --git a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.scss b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.scss index ff4a6ba796..46f9e91757 100644 --- a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.scss +++ b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin login-policy-mfas-theme($theme) { $foreground: map-get($theme, foreground); diff --git a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts index f1f050dc1a..b28a4c3c19 100644 --- a/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts +++ b/console/src/app/modules/policies/login-policy/factor-table/factor-table.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyPaginator as MatPaginator } from '@angular/material/legacy-paginator'; +import { MatDialog } from '@angular/material/dialog'; +import { MatPaginator } from '@angular/material/paginator'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum'; diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index 495ce0f1ad..123bb66aeb 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -1,6 +1,6 @@ import { Component, Injector, Input, OnInit, Type } from '@angular/core'; import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatDialog } from '@angular/material/dialog'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/policies/login-policy/login-policy.module.ts b/console/src/app/modules/policies/login-policy/login-policy.module.ts index 6e244da559..6fc6c24a1a 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.module.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.module.ts @@ -1,15 +1,15 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatRippleModule } from '@angular/material/core'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { CardModule } from 'src/app/modules/card/card.module'; diff --git a/console/src/app/modules/policies/login-texts/login-texts.component.html b/console/src/app/modules/policies/login-texts/login-texts.component.html index ae14b6c135..81421cb28b 100644 --- a/console/src/app/modules/policies/login-texts/login-texts.component.html +++ b/console/src/app/modules/policies/login-texts/login-texts.component.html @@ -18,8 +18,10 @@

@@ -70,7 +72,10 @@ type="submit" mat-stroked-button > - {{ 'ACTIONS.RESETDEFAULT' | translate }} +
+ + {{ 'ACTIONS.RESETDEFAULT' | translate }} +
- - + +
+ + +
- - + +
+ + +
{{ 'ROLESLABEL' | translate }}
- - + -
- {{ role | roletransform }} - -
-
+
+
+ {{ role | roletransform }} + +
+ +
{{ 'ORG.PAGES.PRIMARYDOMAIN' | translate }} - {{ org.primaryDomain }} - +
+ {{ org.primaryDomain }} + +
- - diff --git a/console/src/app/modules/project-roles-table/project-roles-table.component.spec.ts b/console/src/app/modules/project-roles-table/project-roles-table.component.spec.ts index 17332577a1..d37edf5cf7 100644 --- a/console/src/app/modules/project-roles-table/project-roles-table.component.spec.ts +++ b/console/src/app/modules/project-roles-table/project-roles-table.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ProjectRolesTableComponent } from './project-roles-table.component'; diff --git a/console/src/app/modules/project-roles-table/project-roles-table.component.ts b/console/src/app/modules/project-roles-table/project-roles-table.component.ts index 96ff9da551..ae8633d15d 100644 --- a/console/src/app/modules/project-roles-table/project-roles-table.component.ts +++ b/console/src/app/modules/project-roles-table/project-roles-table.component.ts @@ -1,7 +1,7 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyTable as MatTable } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTable } from '@angular/material/table'; import { Router } from '@angular/router'; import { Role } from 'src/app/proto/generated/zitadel/project_pb'; import { ManagementService } from 'src/app/services/mgmt.service'; diff --git a/console/src/app/modules/project-roles-table/project-roles-table.module.ts b/console/src/app/modules/project-roles-table/project-roles-table.module.ts index a39facd539..c30adacd31 100644 --- a/console/src/app/modules/project-roles-table/project-roles-table.module.ts +++ b/console/src/app/modules/project-roles-table/project-roles-table.module.ts @@ -1,14 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/provider-options/provider-options.module.ts b/console/src/app/modules/provider-options/provider-options.module.ts index 5060a0fea3..38d8578417 100644 --- a/console/src/app/modules/provider-options/provider-options.module.ts +++ b/console/src/app/modules/provider-options/provider-options.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { TranslateModule } from '@ngx-translate/core'; import { InfoSectionModule } from '../info-section/info-section.module'; import { ProviderOptionsComponent } from './provider-options.component'; diff --git a/console/src/app/modules/providers/provider-apple/provider-apple.component.html b/console/src/app/modules/providers/provider-apple/provider-apple.component.html index 9a109c2c2f..6e321a5992 100644 --- a/console/src/app/modules/providers/provider-apple/provider-apple.component.html +++ b/console/src/app/modules/providers/provider-apple/provider-apple.component.html @@ -102,17 +102,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-apple/provider-apple.component.ts b/console/src/app/modules/providers/provider-apple/provider-apple.component.ts index 443d586c47..f80a8a8a2c 100644 --- a/console/src/app/modules/providers/provider-apple/provider-apple.component.ts +++ b/console/src/app/modules/providers/provider-apple/provider-apple.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.html b/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.html index 259cdc530c..de0a6707d5 100644 --- a/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.html +++ b/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.html @@ -65,17 +65,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.ts b/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.ts index 6d6ba1f0ff..de4fd8efce 100644 --- a/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.ts +++ b/console/src/app/modules/providers/provider-azure-ad/provider-azure-ad.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.html b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.html index a4511570a6..fc3d08fdb0 100644 --- a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.html +++ b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.html @@ -80,17 +80,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.ts b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.ts index 832fc0b379..276d03eba6 100644 --- a/console/src/app/modules/providers/provider-github-es/provider-github-es.component.ts +++ b/console/src/app/modules/providers/provider-github-es/provider-github-es.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-github/provider-github.component.html b/console/src/app/modules/providers/provider-github/provider-github.component.html index 540c178709..e6aa2ec3a1 100644 --- a/console/src/app/modules/providers/provider-github/provider-github.component.html +++ b/console/src/app/modules/providers/provider-github/provider-github.component.html @@ -61,17 +61,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-github/provider-github.component.ts b/console/src/app/modules/providers/provider-github/provider-github.component.ts index c38ebf5b4a..865e2fc5d7 100644 --- a/console/src/app/modules/providers/provider-github/provider-github.component.ts +++ b/console/src/app/modules/providers/provider-github/provider-github.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.html b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.html index 61df5dfbbc..5adc26b624 100644 --- a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.html +++ b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.html @@ -70,17 +70,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.ts b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.ts index d76d5ac777..2fa7170f1f 100644 --- a/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.ts +++ b/console/src/app/modules/providers/provider-gitlab-self-hosted/provider-gitlab-self-hosted.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.html b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.html index c4ec64094b..9382347b4d 100644 --- a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.html +++ b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.html @@ -60,17 +60,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.ts b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.ts index 1d88f64baa..a6d889525f 100644 --- a/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.ts +++ b/console/src/app/modules/providers/provider-gitlab/provider-gitlab.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-google/provider-google.component.html b/console/src/app/modules/providers/provider-google/provider-google.component.html index a77006c7af..0e9dca638c 100644 --- a/console/src/app/modules/providers/provider-google/provider-google.component.html +++ b/console/src/app/modules/providers/provider-google/provider-google.component.html @@ -59,17 +59,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-google/provider-google.component.ts b/console/src/app/modules/providers/provider-google/provider-google.component.ts index e9ebfef253..0a1160cd69 100644 --- a/console/src/app/modules/providers/provider-google/provider-google.component.ts +++ b/console/src/app/modules/providers/provider-google/provider-google.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html b/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html index da85abb3a3..ea23e101ba 100644 --- a/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html +++ b/console/src/app/modules/providers/provider-oauth/provider-oauth.component.html @@ -84,17 +84,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-oauth/provider-oauth.component.ts b/console/src/app/modules/providers/provider-oauth/provider-oauth.component.ts index c838143e6f..1621df4123 100644 --- a/console/src/app/modules/providers/provider-oauth/provider-oauth.component.ts +++ b/console/src/app/modules/providers/provider-oauth/provider-oauth.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html index 41f8351629..8dbc345a97 100644 --- a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html +++ b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.html @@ -64,17 +64,11 @@ - - + + {{ scope }} cancel - - + + diff --git a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.ts b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.ts index 080a850675..037b2dd6ba 100644 --- a/console/src/app/modules/providers/provider-oidc/provider-oidc.component.ts +++ b/console/src/app/modules/providers/provider-oidc/provider-oidc.component.ts @@ -2,7 +2,7 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, Injector, Type } from '@angular/core'; import { AbstractControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute } from '@angular/router'; import { take } from 'rxjs'; import { diff --git a/console/src/app/modules/providers/providers.module.ts b/console/src/app/modules/providers/providers.module.ts index 47e2679cfb..5af3fee6a8 100644 --- a/console/src/app/modules/providers/providers.module.ts +++ b/console/src/app/modules/providers/providers.module.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; @@ -64,7 +64,7 @@ import { ProvidersRoutingModule } from './providers-routing.module'; MatTooltipModule, TranslateModule, ProviderOptionsModule, - MatLegacyProgressSpinnerModule, + MatProgressSpinnerModule, ], }) export default class ProvidersModule {} diff --git a/console/src/app/modules/providers/providers.scss b/console/src/app/modules/providers/providers.scss index 8e96c3428e..0fb8879376 100644 --- a/console/src/app/modules/providers/providers.scss +++ b/console/src/app/modules/providers/providers.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin identity-provider-theme($theme) { $is-dark-theme: map-get($theme, is-dark); $background: map-get($theme, background); @@ -64,7 +62,7 @@ font-size: 12px; } - .mat-chip-input { + .mat-mdc-chip-input { width: 100%; margin: 0; } @@ -181,6 +179,7 @@ button[mat-raised-button] { border-radius: 0.5rem; padding: 0.5rem 4rem; + height: 3.5rem; } } diff --git a/console/src/app/modules/refresh-table/refresh-table.component.html b/console/src/app/modules/refresh-table/refresh-table.component.html index 895e937093..b971bb164c 100644 --- a/console/src/app/modules/refresh-table/refresh-table.component.html +++ b/console/src/app/modules/refresh-table/refresh-table.component.html @@ -6,15 +6,17 @@ | - - {{ 'ORG_DETAIL.TABLE.CLEAR' | translate }} - - + +
+ {{ 'ORG_DETAIL.TABLE.CLEAR' | translate }} + + +
diff --git a/console/src/app/modules/refresh-table/refresh-table.component.scss b/console/src/app/modules/refresh-table/refresh-table.component.scss index dbed9fc327..096aab6c85 100644 --- a/console/src/app/modules/refresh-table/refresh-table.component.scss +++ b/console/src/app/modules/refresh-table/refresh-table.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin refresh-table-theme($theme) { $foreground: map-get($theme, foreground); @@ -48,6 +46,8 @@ .icon { font-size: 1.2rem; + height: 1.2rem; + width: 1.2rem; } } } diff --git a/console/src/app/modules/refresh-table/refresh-table.module.ts b/console/src/app/modules/refresh-table/refresh-table.module.ts index 0d797501fd..36fffce9b6 100644 --- a/console/src/app/modules/refresh-table/refresh-table.module.ts +++ b/console/src/app/modules/refresh-table/refresh-table.module.ts @@ -1,10 +1,10 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { PaginatorModule } from 'src/app/modules/paginator/paginator.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; diff --git a/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.component.ts b/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.component.ts index b3139d7b56..cf8164bd58 100644 --- a/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.component.ts +++ b/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.component.ts @@ -1,7 +1,6 @@ import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; -import { MatLegacyAutocomplete as MatAutocomplete } from '@angular/material/legacy-autocomplete'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { debounceTime, from, map, Subject, switchMap, takeUntil, tap } from 'rxjs'; import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; import { Org, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb'; @@ -14,7 +13,6 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; styleUrls: ['./search-org-autocomplete.component.scss'], }) export class SearchOrgAutocompleteComponent implements OnInit, OnDestroy { - public selectable: boolean = true; public myControl: UntypedFormControl = new UntypedFormControl(); public filteredOrgs: Array = []; public isLoading: boolean = false; diff --git a/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.module.ts b/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.module.ts index 36c99d27f3..26bf94292c 100644 --- a/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.module.ts +++ b/console/src/app/modules/search-org-autocomplete/search-org-autocomplete.module.ts @@ -1,12 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; diff --git a/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.component.ts b/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.component.ts index 8a4e39ff97..c6613f3951 100644 --- a/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.component.ts +++ b/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.component.ts @@ -1,11 +1,8 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { - MatLegacyAutocomplete as MatAutocomplete, - MatLegacyAutocompleteSelectedEvent as MatAutocompleteSelectedEvent, -} from '@angular/material/legacy-autocomplete'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; import { forkJoin, from, Subject } from 'rxjs'; import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators'; import { ListProjectGrantsResponse, ListProjectsResponse } from 'src/app/proto/generated/zitadel/management_pb'; @@ -26,7 +23,6 @@ export enum ProjectAutocompleteType { styleUrls: ['./search-project-autocomplete.component.scss'], }) export class SearchProjectAutocompleteComponent implements OnInit, OnDestroy { - public selectable: boolean = true; public removable: boolean = true; public addOnBlur: boolean = true; public separatorKeysCodes: number[] = [ENTER, COMMA]; diff --git a/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.module.ts b/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.module.ts index e018e2708a..2a170e34a5 100644 --- a/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.module.ts +++ b/console/src/app/modules/search-project-autocomplete/search-project-autocomplete.module.ts @@ -1,12 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; diff --git a/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.html b/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.html index 5fa9c90517..77b73f224f 100644 --- a/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.html +++ b/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.html @@ -11,17 +11,11 @@ [matAutocomplete]="auto" /> - - + + {{ selectedRole.displayName }} cancel - + - + diff --git a/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.ts b/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.ts index 255103ea45..3aea4bb4da 100644 --- a/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.ts +++ b/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.component.ts @@ -1,11 +1,8 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { - MatLegacyAutocomplete as MatAutocomplete, - MatLegacyAutocompleteSelectedEvent as MatAutocompleteSelectedEvent, -} from '@angular/material/legacy-autocomplete'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; import { from, Subject } from 'rxjs'; import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators'; import { Role, RoleDisplayNameQuery, RoleQuery } from 'src/app/proto/generated/zitadel/project_pb'; @@ -17,7 +14,6 @@ import { ManagementService } from 'src/app/services/mgmt.service'; styleUrls: ['./search-roles-autocomplete.component.scss'], }) export class SearchRolesAutocompleteComponent implements OnDestroy { - public selectable: boolean = true; public removable: boolean = true; public addOnBlur: boolean = true; public separatorKeysCodes: number[] = [ENTER, COMMA]; diff --git a/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.module.ts b/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.module.ts index c4ac3214b9..cff2e7a22c 100644 --- a/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.module.ts +++ b/console/src/app/modules/search-roles-autocomplete/search-roles-autocomplete.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from 'src/app/modules/input/input.module'; diff --git a/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.scss b/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.scss index 78ecaee09a..4f2b918838 100644 --- a/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.scss +++ b/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.scss @@ -1,17 +1,15 @@ -@use '@angular/material' as mat; - @mixin search-user-autocomplete-theme($theme) { $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); - $lighter-primary-color: mat.get-color-from-palette($primary, 300); - $darker-primary-color: mat.get-color-from-palette($primary, 700); + $primary-color: map-get($primary, 500); + $lighter-primary-color: map-get($primary, 300); + $darker-primary-color: map-get($primary, 700); $background: map-get($theme, background); $foreground: map-get($theme, foreground); $secondary-text: map-get($foreground, secondary-text); $is-dark-theme: map-get($theme, is-dark); - $link-hover-color: if($is-dark-theme, mat.get-color-from-palette($primary, 200), $primary-color); + $link-hover-color: if($is-dark-theme, map-get($primary, 200), $primary-color); $link-color: if($is-dark-theme, $lighter-primary-color, $primary-color); .user-autocomplete-found { diff --git a/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.ts b/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.ts index 987e9f2263..aec06a0416 100644 --- a/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.ts +++ b/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.component.ts @@ -11,11 +11,8 @@ import { ViewChild, } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { - MatLegacyAutocomplete as MatAutocomplete, - MatLegacyAutocompleteSelectedEvent as MatAutocompleteSelectedEvent, -} from '@angular/material/legacy-autocomplete'; -import { MatLegacyChipInputEvent as MatChipInputEvent } from '@angular/material/legacy-chips'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; import { from, of, Subject } from 'rxjs'; import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators'; import { ListUsersResponse } from 'src/app/proto/generated/zitadel/management_pb'; @@ -35,7 +32,6 @@ export enum UserTarget { styleUrls: ['./search-user-autocomplete.component.scss'], }) export class SearchUserAutocompleteComponent implements OnInit, AfterContentChecked { - public selectable: boolean = true; public removable: boolean = true; public addOnBlur: boolean = true; public separatorKeysCodes: number[] = [ENTER, COMMA]; diff --git a/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.module.ts b/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.module.ts index 5d6b0453b1..f4f19e6faa 100644 --- a/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.module.ts +++ b/console/src/app/modules/search-user-autocomplete/search-user-autocomplete.module.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { AvatarModule } from '../avatar/avatar.module'; diff --git a/console/src/app/modules/settings-grid/settings-grid.module.ts b/console/src/app/modules/settings-grid/settings-grid.module.ts index 24fc0afd0e..59c1f5dcfc 100644 --- a/console/src/app/modules/settings-grid/settings-grid.module.ts +++ b/console/src/app/modules/settings-grid/settings-grid.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/shortcuts/shortcuts.component.scss b/console/src/app/modules/shortcuts/shortcuts.component.scss index 775fc56a90..5dce518130 100644 --- a/console/src/app/modules/shortcuts/shortcuts.component.scss +++ b/console/src/app/modules/shortcuts/shortcuts.component.scss @@ -1,20 +1,18 @@ -@use '@angular/material' as mat; - @mixin shortcut-theme($theme) { $primary: map-get($theme, primary); $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); - $list-background-color: mat.get-color-from-palette($background, 300); - $card-background-color: mat.get-color-from-palette($background, cards); + $list-background-color: map-get($background, 300); + $card-background-color: map-get($background, cards); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); $border-selected-color: if($is-dark-theme, #fff, #000); diff --git a/console/src/app/modules/shortcuts/shortcuts.module.ts b/console/src/app/modules/shortcuts/shortcuts.module.ts index 57c593e23e..a48f02dc25 100644 --- a/console/src/app/modules/shortcuts/shortcuts.module.ts +++ b/console/src/app/modules/shortcuts/shortcuts.module.ts @@ -1,9 +1,9 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts b/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts index 7b0c999cf0..e6655ae29b 100644 --- a/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts +++ b/console/src/app/modules/show-key-dialog/show-key-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { saveAs } from 'file-saver'; import { AddAppKeyResponse, AddMachineKeyResponse } from 'src/app/proto/generated/zitadel/management_pb'; import { InfoSectionType } from '../info-section/info-section.component'; diff --git a/console/src/app/modules/show-key-dialog/show-key-dialog.module.ts b/console/src/app/modules/show-key-dialog/show-key-dialog.module.ts index 14e7523e22..66a3178747 100644 --- a/console/src/app/modules/show-key-dialog/show-key-dialog.module.ts +++ b/console/src/app/modules/show-key-dialog/show-key-dialog.module.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatButtonModule } from '@angular/material/button'; import { TranslateModule } from '@ngx-translate/core'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; import { InfoSectionModule } from '../info-section/info-section.module'; +import { MatDialogModule } from '@angular/material/dialog'; import { ShowKeyDialogComponent } from './show-key-dialog.component'; @NgModule({ @@ -15,6 +16,7 @@ import { ShowKeyDialogComponent } from './show-key-dialog.component'; TranslateModule, MatButtonModule, LocalizedDatePipeModule, + MatDialogModule, InfoSectionModule, TimestampToDatePipeModule, ], diff --git a/console/src/app/modules/show-token-dialog/show-token-dialog.component.ts b/console/src/app/modules/show-token-dialog/show-token-dialog.component.ts index c42f659006..4ba5d3b508 100644 --- a/console/src/app/modules/show-token-dialog/show-token-dialog.component.ts +++ b/console/src/app/modules/show-token-dialog/show-token-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { AddPersonalAccessTokenResponse } from 'src/app/proto/generated/zitadel/management_pb'; import { InfoSectionType } from '../info-section/info-section.component'; diff --git a/console/src/app/modules/show-token-dialog/show-token-dialog.module.ts b/console/src/app/modules/show-token-dialog/show-token-dialog.module.ts index 207b3a1d2a..78ddb6ee51 100644 --- a/console/src/app/modules/show-token-dialog/show-token-dialog.module.ts +++ b/console/src/app/modules/show-token-dialog/show-token-dialog.module.ts @@ -1,12 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; +import { MatDialogModule } from '@angular/material/dialog'; import { InfoSectionModule } from '../info-section/info-section.module'; import { ShowTokenDialogComponent } from './show-token-dialog.component'; @@ -15,6 +16,7 @@ import { ShowTokenDialogComponent } from './show-token-dialog.component'; imports: [ CommonModule, TranslateModule, + MatDialogModule, InfoSectionModule, CopyToClipboardModule, MatButtonModule, diff --git a/console/src/app/modules/sidenav/sidenav.component.scss b/console/src/app/modules/sidenav/sidenav.component.scss index cc0caf57dd..383857751c 100644 --- a/console/src/app/modules/sidenav/sidenav.component.scss +++ b/console/src/app/modules/sidenav/sidenav.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin sidenav-theme($theme) { $foreground: map-get($theme, foreground); $background: map-get($theme, background); @@ -133,7 +131,6 @@ .sidenav-content { width: 100%; - overflow-x: auto; } @media only screen and (min-width: 824px) { diff --git a/console/src/app/modules/string-list/string-list.component.scss b/console/src/app/modules/string-list/string-list.component.scss index d44143914a..b5309d5964 100644 --- a/console/src/app/modules/string-list/string-list.component.scss +++ b/console/src/app/modules/string-list/string-list.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin string-list-theme($theme) { $foreground: map-get($theme, foreground); $background: map-get($theme, background); @@ -11,7 +9,7 @@ display: flex; flex-direction: row; max-width: 400px; - background: if($is-dark-theme, #00000020, mat.get-color-from-palette($background, cards)); + background: if($is-dark-theme, #00000020, map-get($background, cards)); margin-left: -1rem; margin-right: -1rem; padding: 0 1rem 0.5rem 1rem; diff --git a/console/src/app/modules/string-list/string-list.module.ts b/console/src/app/modules/string-list/string-list.module.ts index db9322641b..fb9d7938c8 100644 --- a/console/src/app/modules/string-list/string-list.module.ts +++ b/console/src/app/modules/string-list/string-list.module.ts @@ -1,10 +1,10 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { InputModule } from '../input/input.module'; import { StringListComponent } from './string-list.component'; @@ -16,11 +16,11 @@ import { StringListComponent } from './string-list.component'; InputModule, FormsModule, ReactiveFormsModule, - MatLegacyChipsModule, + MatChipsModule, TranslateModule, MatIconModule, - MatLegacyTooltipModule, - MatLegacyButtonModule, + MatTooltipModule, + MatButtonModule, ], exports: [StringListComponent], }) diff --git a/console/src/app/modules/table-actions/table-actions.component.html b/console/src/app/modules/table-actions/table-actions.component.html index 5032f4dace..df19103af0 100644 --- a/console/src/app/modules/table-actions/table-actions.component.html +++ b/console/src/app/modules/table-actions/table-actions.component.html @@ -5,12 +5,12 @@ diff --git a/console/src/app/modules/table-actions/table-actions.component.scss b/console/src/app/modules/table-actions/table-actions.component.scss index 0604b7ad8b..30018229b8 100644 --- a/console/src/app/modules/table-actions/table-actions.component.scss +++ b/console/src/app/modules/table-actions/table-actions.component.scss @@ -1,19 +1,14 @@ -@use '@angular/material' as mat; - @mixin table-actions-theme($theme) { $background: map-get($theme, background); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); - $card-background-color: mat.get-color-from-palette($background, cards); + $card-background-color: map-get($background, cards); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); .cnsl-table-action-wrapper { - position: relative; height: 36px; .cnsl-table-action { - position: absolute; - right: 0; display: flex; background-color: $card-background-color; transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); @@ -25,6 +20,9 @@ box-shadow: 0 0 3px #0000001a; height: 36px; align-items: center; + width: fit-content; + float: right; + overflow: hidden; button { height: 36px; @@ -32,6 +30,12 @@ display: flex; align-items: center; justify-content: center; + + &.more-button { + font-size: 1rem; + line-height: 1.5rem; + padding: 0; + } } button:only-of-type { diff --git a/console/src/app/modules/table-actions/table-actions.module.ts b/console/src/app/modules/table-actions/table-actions.module.ts index ad92bcc814..2d249d70f2 100644 --- a/console/src/app/modules/table-actions/table-actions.module.ts +++ b/console/src/app/modules/table-actions/table-actions.module.ts @@ -1,9 +1,9 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { TableActionsComponent } from './table-actions.component'; diff --git a/console/src/app/modules/theme-setting/theme-setting.component.html b/console/src/app/modules/theme-setting/theme-setting.component.html index 37f09ad1bf..4ed050be7e 100644 --- a/console/src/app/modules/theme-setting/theme-setting.component.html +++ b/console/src/app/modules/theme-setting/theme-setting.component.html @@ -1,13 +1,34 @@ - +
+ + +
diff --git a/console/src/app/modules/theme-setting/theme-setting.component.scss b/console/src/app/modules/theme-setting/theme-setting.component.scss index c477935803..0950d84a4e 100644 --- a/console/src/app/modules/theme-setting/theme-setting.component.scss +++ b/console/src/app/modules/theme-setting/theme-setting.component.scss @@ -1,12 +1,57 @@ -@use '@angular/material' as mat; - @mixin theme-setting($theme) { $primary: map-get($theme, primary); $background: map-get($theme, background); + $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); - .theme-setting-button { - background-color: if($is-dark-theme, #ffffff10, #fff) !important; + .theme-flex { + display: flex; + align-items: center; + border: 1px solid if($is-dark-theme, #ffffff20, #00000020); + border-radius: 50vw; + padding: 0.25rem; + + .theme-setting-button { + background-color: transparent; + border-radius: 50vw; + display: flex; + align-items: center; + justify-content: center; + height: 2rem; + width: 2rem; + border: none; + cursor: pointer; + padding: 0; + + .moon, + .sun { + color: map-get($foreground, text); + fill: transparent; + height: 1rem; + width: 1rem; + opacity: 0.7; + } + + .sun { + height: 1.5rem; + width: 1.5rem; + } + + &:hover { + .moon, + .sun { + opacity: 1; + } + } + + &.active { + background-color: if($is-dark-theme, #ffffff10, #00000010) !important; + .moon, + .sun { + opacity: 1; + } + } + } } } diff --git a/console/src/app/modules/theme-setting/theme-setting.module.ts b/console/src/app/modules/theme-setting/theme-setting.module.ts index 6efa4f45f8..b440a3cfb2 100644 --- a/console/src/app/modules/theme-setting/theme-setting.module.ts +++ b/console/src/app/modules/theme-setting/theme-setting.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; import { TranslateModule } from '@ngx-translate/core'; import { ThemeSettingComponent } from './theme-setting.component'; diff --git a/console/src/app/modules/top-view/top-view.component.html b/console/src/app/modules/top-view/top-view.component.html index 63e85e962d..65e68dec7a 100644 --- a/console/src/app/modules/top-view/top-view.component.html +++ b/console/src/app/modules/top-view/top-view.component.html @@ -18,7 +18,7 @@ @@ -29,13 +29,15 @@ - add - {{ 'GRANTS.ADD_BTN' | translate }} - +
+ add + {{ 'GRANTS.ADD_BTN' | translate }} + +
- - + +
+ + +
- - + +
+ + +
- - diff --git a/console/src/app/modules/user-grants/user-grants.component.scss b/console/src/app/modules/user-grants/user-grants.component.scss index 5e9a315996..fca43cf99c 100644 --- a/console/src/app/modules/user-grants/user-grants.component.scss +++ b/console/src/app/modules/user-grants/user-grants.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin user-grants-theme($theme) { $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); diff --git a/console/src/app/modules/user-grants/user-grants.component.ts b/console/src/app/modules/user-grants/user-grants.component.ts index 4003458657..f3c5521fae 100644 --- a/console/src/app/modules/user-grants/user-grants.component.ts +++ b/console/src/app/modules/user-grants/user-grants.component.ts @@ -1,8 +1,8 @@ import { SelectionModel } from '@angular/cdk/collections'; import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyInput as MatInput } from '@angular/material/legacy-input'; -import { MatLegacyTable as MatTable } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; +import { MatInput } from '@angular/material/input'; +import { MatTable } from '@angular/material/table'; import { Router } from '@angular/router'; import { tap } from 'rxjs/operators'; import { enterAnimations } from 'src/app/animations'; diff --git a/console/src/app/modules/user-grants/user-grants.module.ts b/console/src/app/modules/user-grants/user-grants.module.ts index 3bdb7464ad..d076ec28ad 100644 --- a/console/src/app/modules/user-grants/user-grants.module.ts +++ b/console/src/app/modules/user-grants/user-grants.module.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/modules/warn-dialog/warn-dialog.component.html b/console/src/app/modules/warn-dialog/warn-dialog.component.html index 17c21f28b6..56370d8944 100644 --- a/console/src/app/modules/warn-dialog/warn-dialog.component.html +++ b/console/src/app/modules/warn-dialog/warn-dialog.component.html @@ -1,13 +1,16 @@ -{{ data.titleKey | translate: data.titleParam }} +

+ {{ data.titleKey | translate: data.titleParam }} +

+

{{ data.descriptionKey | translate: data.descriptionParam }}

- {{ - data.warnSectionKey | translate - }} + + {{ data.warnSectionKey | translate }} +

{{ data.hintKey | translate: { value: data.confirmation } }}

@@ -16,7 +19,8 @@
-
+ +
diff --git a/console/src/app/modules/warn-dialog/warn-dialog.component.scss b/console/src/app/modules/warn-dialog/warn-dialog.component.scss index 5308d2266d..3e9d17ae7c 100644 --- a/console/src/app/modules/warn-dialog/warn-dialog.component.scss +++ b/console/src/app/modules/warn-dialog/warn-dialog.component.scss @@ -1,6 +1,6 @@ -.title { - font-size: 1.2rem; - margin-top: 0; +h1 { + font-size: 1.5rem; + margin: 0; } .icon-wrapper { diff --git a/console/src/app/modules/warn-dialog/warn-dialog.component.ts b/console/src/app/modules/warn-dialog/warn-dialog.component.ts index 701fe8f398..5a0037525e 100644 --- a/console/src/app/modules/warn-dialog/warn-dialog.component.ts +++ b/console/src/app/modules/warn-dialog/warn-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { InfoSectionType } from '../info-section/info-section.component'; diff --git a/console/src/app/modules/warn-dialog/warn-dialog.module.ts b/console/src/app/modules/warn-dialog/warn-dialog.module.ts index 3100984cb9..d8e5024d78 100644 --- a/console/src/app/modules/warn-dialog/warn-dialog.module.ts +++ b/console/src/app/modules/warn-dialog/warn-dialog.module.ts @@ -1,15 +1,16 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatButtonModule } from '@angular/material/button'; import { TranslateModule } from '@ngx-translate/core'; +import { MatDialogModule } from '@angular/material/dialog'; import { InfoSectionModule } from '../info-section/info-section.module'; import { InputModule } from '../input/input.module'; import { WarnDialogComponent } from './warn-dialog.component'; @NgModule({ declarations: [WarnDialogComponent], - imports: [CommonModule, FormsModule, TranslateModule, InfoSectionModule, MatButtonModule, InputModule], + imports: [CommonModule, FormsModule, MatDialogModule, TranslateModule, InfoSectionModule, MatButtonModule, InputModule], }) export class WarnDialogModule {} diff --git a/console/src/app/pages/actions/action-table/action-table.component.html b/console/src/app/pages/actions/action-table/action-table.component.html index 3d5ed09cb0..1f68faa6f4 100644 --- a/console/src/app/pages/actions/action-table/action-table.component.html +++ b/console/src/app/pages/actions/action-table/action-table.component.html @@ -7,38 +7,43 @@ [selection]="selection" >
- -
- - - - - +
+ - - - - - + +
+ +
+
+ + + + + + + + +
- -
- - + +
+ + +
+ - diff --git a/console/src/app/pages/actions/actions.component.scss b/console/src/app/pages/actions/actions.component.scss index 104c37257d..df06575c2c 100644 --- a/console/src/app/pages/actions/actions.component.scss +++ b/console/src/app/pages/actions/actions.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin actions-theme($theme) { $foreground: map-get($theme, foreground); $background: map-get($theme, background); diff --git a/console/src/app/pages/actions/actions.component.ts b/console/src/app/pages/actions/actions.component.ts index bc1dcc43b8..7d5ac14ca1 100644 --- a/console/src/app/pages/actions/actions.component.ts +++ b/console/src/app/pages/actions/actions.component.ts @@ -1,7 +1,7 @@ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { Component, OnDestroy } from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatDialog } from '@angular/material/dialog'; import { Subject, takeUntil } from 'rxjs'; import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component'; import { InfoSectionType } from 'src/app/modules/info-section/info-section.component'; diff --git a/console/src/app/pages/actions/actions.module.ts b/console/src/app/pages/actions/actions.module.ts index 64a93c160d..c5da8663fa 100644 --- a/console/src/app/pages/actions/actions.module.ts +++ b/console/src/app/pages/actions/actions.module.ts @@ -2,13 +2,13 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { CodemirrorModule } from '@ctrl/ngx-codemirror'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.scss b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.scss index dce51a6de5..022729aef8 100644 --- a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.scss +++ b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.scss @@ -1,17 +1,8 @@ -@use '@angular/material' as mat; - .action-dialog-title { font-size: 1.2rem; margin-top: 0; } -@mixin action-dialog-theme($theme) { - $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); - $is-dark-theme: map-get($theme, is-dark); - $foreground: map-get($theme, foreground); -} - .action { display: flex; align-items: center; diff --git a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.ts b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.ts index 46d0232b86..7038acbb61 100644 --- a/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.ts +++ b/console/src/app/pages/actions/add-action-dialog/add-action-dialog.component.ts @@ -1,10 +1,6 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { - MatLegacyDialog as MatDialog, - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; import { mapTo, Subject, takeUntil } from 'rxjs'; diff --git a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.html b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.html index 63f2df1537..2a8c36bb63 100644 --- a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.html +++ b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.html @@ -22,6 +22,7 @@
+ diff --git a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.scss b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.scss index 1c37381905..06a9a313ae 100644 --- a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.scss +++ b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.scss @@ -9,9 +9,12 @@ .action { display: flex; - justify-content: space-between; margin-top: 1rem; + .fill-space { + flex: 1; + } + .ok-button { margin-left: 0.5rem; } diff --git a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.ts b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.ts index 49f4a98d16..0b8cffd940 100644 --- a/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.ts +++ b/console/src/app/pages/actions/add-flow-dialog/add-flow-dialog.component.ts @@ -1,9 +1,6 @@ import { Component, Inject } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { requiredValidator } from 'src/app/modules/form-field/validators/validators'; import { Action, FlowType, TriggerType } from 'src/app/proto/generated/zitadel/action_pb'; import { SetTriggerActionsRequest } from 'src/app/proto/generated/zitadel/management_pb'; diff --git a/console/src/app/pages/app-create/app-create.component.scss b/console/src/app/pages/app-create/app-create.component.scss index 4050c25873..d8d3127bd8 100644 --- a/console/src/app/pages/app-create/app-create.component.scss +++ b/console/src/app/pages/app-create/app-create.component.scss @@ -13,8 +13,8 @@ h1 { .continue-button { margin-top: 3rem; display: block; - padding: 0.5rem 4rem; - border-radius: 0.5rem; + height: 3.5rem; + padding: 0 4rem; } } diff --git a/console/src/app/pages/app-create/app-create.module.ts b/console/src/app/pages/app-create/app-create.module.ts index 502446297d..fa7a0aa4de 100644 --- a/console/src/app/pages/app-create/app-create.module.ts +++ b/console/src/app/pages/app-create/app-create.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { CreateLayoutModule } from 'src/app/modules/create-layout/create-layout.module'; diff --git a/console/src/app/pages/events/events.component.scss b/console/src/app/pages/events/events.component.scss index d80824d327..ee275a38ce 100644 --- a/console/src/app/pages/events/events.component.scss +++ b/console/src/app/pages/events/events.component.scss @@ -1,13 +1,11 @@ -@use '@angular/material' as mat; - @mixin events-theme($theme) { $background: map-get($theme, background); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); - $card-background-color: mat.get-color-from-palette($background, cards); + $card-background-color: map-get($background, cards); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); $primary: map-get($theme, primary); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); .mat-column-payload { position: relative; diff --git a/console/src/app/pages/events/events.component.ts b/console/src/app/pages/events/events.component.ts index d5e1a6ef14..41f520a7a4 100644 --- a/console/src/app/pages/events/events.component.ts +++ b/console/src/app/pages/events/events.component.ts @@ -1,8 +1,8 @@ import { LiveAnnouncer } from '@angular/cdk/a11y'; import { Component, OnDestroy, ViewChild } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { MatDialog } from '@angular/material/dialog'; import { MatSort, Sort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; import { BehaviorSubject, Observable, Subject, takeUntil } from 'rxjs'; import { DisplayJsonDialogComponent } from 'src/app/modules/display-json-dialog/display-json-dialog.component'; import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component'; diff --git a/console/src/app/pages/events/events.module.ts b/console/src/app/pages/events/events.module.ts index b22a12dca4..b1ba802f39 100644 --- a/console/src/app/pages/events/events.module.ts +++ b/console/src/app/pages/events/events.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { CardModule } from 'src/app/modules/card/card.module'; @@ -19,7 +19,7 @@ import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/local import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; import { OverlayModule } from '@angular/cdk/overlay'; -import { MatLegacyDialogModule } from '@angular/material/legacy-dialog'; +import { MatDialogModule } from '@angular/material/dialog'; import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module'; import { AvatarModule } from 'src/app/modules/avatar/avatar.module'; import { DisplayJsonDialogModule } from 'src/app/modules/display-json-dialog/display-json-dialog.module'; @@ -41,7 +41,7 @@ import { EventsComponent } from './events.component'; ToObjectPipeModule, ToPayloadPipeModule, HasRolePipeModule, - MatLegacyDialogModule, + MatDialogModule, MatButtonModule, CopyToClipboardModule, InputModule, diff --git a/console/src/app/pages/failed-events/failed-events.component.ts b/console/src/app/pages/failed-events/failed-events.component.ts index cb764c24a9..c3f5fedb46 100644 --- a/console/src/app/pages/failed-events/failed-events.component.ts +++ b/console/src/app/pages/failed-events/failed-events.component.ts @@ -1,6 +1,6 @@ import { AfterViewInit, Component, ViewChild } from '@angular/core'; -import { MatLegacyPaginator as MatPaginator } from '@angular/material/legacy-paginator'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatTableDataSource } from '@angular/material/table'; import { BehaviorSubject, from, Observable, of } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; import { FailedEvent } from 'src/app/proto/generated/zitadel/admin_pb'; diff --git a/console/src/app/pages/failed-events/failed-events.module.ts b/console/src/app/pages/failed-events/failed-events.module.ts index 8fc8b7a5ef..39954791e6 100644 --- a/console/src/app/pages/failed-events/failed-events.module.ts +++ b/console/src/app/pages/failed-events/failed-events.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { CardModule } from 'src/app/modules/card/card.module'; diff --git a/console/src/app/pages/home/home.component.scss b/console/src/app/pages/home/home.component.scss index 68a8a8742c..8dac8e43c5 100644 --- a/console/src/app/pages/home/home.component.scss +++ b/console/src/app/pages/home/home.component.scss @@ -1,20 +1,18 @@ -@use '@angular/material' as mat; - @mixin home-theme($theme) { $primary: map-get($theme, primary); $warn: map-get($theme, warn); $background: map-get($theme, background); $accent: map-get($theme, accent); - $primary-color: mat.get-color-from-palette($primary, 500); + $primary-color: map-get($primary, 500); - $warn-color: mat.get-color-from-palette($warn, 500); - $accent-color: mat.get-color-from-palette($accent, 500); + $warn-color: map-get($warn, 500); + $accent-color: map-get($accent, 500); $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); $back: map-get($background, background); - $list-background-color: mat.get-color-from-palette($background, 300); - $card-background-color: mat.get-color-from-palette($background, cards); + $list-background-color: map-get($background, 300); + $card-background-color: map-get($background, cards); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); $border-selected-color: if($is-dark-theme, #fff, #000); diff --git a/console/src/app/pages/home/home.module.ts b/console/src/app/pages/home/home.module.ts index 8dbbb5daef..5184b40112 100644 --- a/console/src/app/pages/home/home.module.ts +++ b/console/src/app/pages/home/home.module.ts @@ -1,10 +1,10 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatRippleModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { ShortcutsModule } from 'src/app/modules/shortcuts/shortcuts.module'; diff --git a/console/src/app/pages/iam-views/iam-views.component.ts b/console/src/app/pages/iam-views/iam-views.component.ts index 91913e5cb7..41e5b87037 100644 --- a/console/src/app/pages/iam-views/iam-views.component.ts +++ b/console/src/app/pages/iam-views/iam-views.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, Component, ViewChild } from '@angular/core'; -import { MatLegacyPaginator as MatPaginator } from '@angular/material/legacy-paginator'; -import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; import { BehaviorSubject, from, Observable, of } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; import { View } from 'src/app/proto/generated/zitadel/admin_pb'; diff --git a/console/src/app/pages/iam-views/iam-views.module.ts b/console/src/app/pages/iam-views/iam-views.module.ts index 3662f87ace..13c30ba8df 100644 --- a/console/src/app/pages/iam-views/iam-views.module.ts +++ b/console/src/app/pages/iam-views/iam-views.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { CardModule } from 'src/app/modules/card/card.module'; diff --git a/console/src/app/pages/instance/instance-members/instance-members.component.html b/console/src/app/pages/instance/instance-members/instance-members.component.html index c3181671f1..e6a58e2270 100644 --- a/console/src/app/pages/instance/instance-members/instance-members.component.html +++ b/console/src/app/pages/instance/instance-members/instance-members.component.html @@ -2,7 +2,7 @@

{{ 'IAM.MEMBER.DESCRIPTION' | translate }} - + info_outline

- diff --git a/console/src/app/pages/instance/instance-members/instance-members.component.scss b/console/src/app/pages/instance/instance-members/instance-members.component.scss index bdf7cb3cf8..7647171923 100644 --- a/console/src/app/pages/instance/instance-members/instance-members.component.scss +++ b/console/src/app/pages/instance/instance-members/instance-members.component.scss @@ -4,7 +4,7 @@ font-size: 14px; margin: -1.5rem 0 0 0; - i { + .icon { font-size: 1.2rem; height: 1.2rem; line-height: 1.2rem; diff --git a/console/src/app/pages/instance/instance-members/instance-members.component.spec.ts b/console/src/app/pages/instance/instance-members/instance-members.component.spec.ts index 4f55067766..204bb8803f 100644 --- a/console/src/app/pages/instance/instance-members/instance-members.component.spec.ts +++ b/console/src/app/pages/instance/instance-members/instance-members.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { InstanceMembersComponent } from './instance-members.component'; diff --git a/console/src/app/pages/instance/instance-members/instance-members.component.ts b/console/src/app/pages/instance/instance-members/instance-members.component.ts index cbfa759df3..2fb9755d41 100644 --- a/console/src/app/pages/instance/instance-members/instance-members.component.ts +++ b/console/src/app/pages/instance/instance-members/instance-members.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { LegacyPageEvent as PageEvent } from '@angular/material/legacy-paginator'; +import { MatDialog } from '@angular/material/dialog'; +import { PageEvent } from '@angular/material/paginator'; import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { Member } from 'src/app/proto/generated/zitadel/member_pb'; diff --git a/console/src/app/pages/instance/instance-members/instance-members.module.ts b/console/src/app/pages/instance/instance-members/instance-members.module.ts index 3515590ac5..46fda95a90 100644 --- a/console/src/app/pages/instance/instance-members/instance-members.module.ts +++ b/console/src/app/pages/instance/instance-members/instance-members.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module'; diff --git a/console/src/app/pages/instance/instance.component.scss b/console/src/app/pages/instance/instance.component.scss index ea9a2c1a1c..4c8cf2338e 100644 --- a/console/src/app/pages/instance/instance.component.scss +++ b/console/src/app/pages/instance/instance.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin instance-detail-theme($theme) { $foreground: map-get($theme, foreground); $is-dark-theme: map-get($theme, is-dark); diff --git a/console/src/app/pages/instance/instance.component.ts b/console/src/app/pages/instance/instance.component.ts index 3202efac38..a0f7cc1a8d 100644 --- a/console/src/app/pages/instance/instance.component.ts +++ b/console/src/app/pages/instance/instance.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, from, Observable, of } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; diff --git a/console/src/app/pages/instance/instance.module.ts b/console/src/app/pages/instance/instance.module.ts index 594ff853b2..c4a67540ef 100644 --- a/console/src/app/pages/instance/instance.module.ts +++ b/console/src/app/pages/instance/instance.module.ts @@ -1,16 +1,16 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { CardModule } from 'src/app/modules/card/card.module'; diff --git a/console/src/app/pages/org-create/org-create.component.scss b/console/src/app/pages/org-create/org-create.component.scss index d29ffc3a45..861ff34bd0 100644 --- a/console/src/app/pages/org-create/org-create.component.scss +++ b/console/src/app/pages/org-create/org-create.component.scss @@ -34,7 +34,8 @@ h1 { .continue-button { margin-top: 3rem; display: block; - padding: 0.5rem 4rem; + height: 3.5rem; + padding: 0 4rem; } } @@ -115,7 +116,8 @@ h1 { .big-button { display: block; - padding: 0.5rem 4rem; + padding: 0 4rem; + height: 3.5rem; } } diff --git a/console/src/app/pages/org-create/org-create.component.ts b/console/src/app/pages/org-create/org-create.component.ts index 200ff4941c..7b0fc73e7b 100644 --- a/console/src/app/pages/org-create/org-create.component.ts +++ b/console/src/app/pages/org-create/org-create.component.ts @@ -2,7 +2,7 @@ import { animate, style, transition, trigger } from '@angular/animations'; import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; -import { MatLegacySlideToggleChange as MatSlideToggleChange } from '@angular/material/legacy-slide-toggle'; +import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { Router } from '@angular/router'; import { containsLowerCaseValidator, diff --git a/console/src/app/pages/org-create/org-create.module.ts b/console/src/app/pages/org-create/org-create.module.ts index ec9cf3f0c1..889142e8b7 100644 --- a/console/src/app/pages/org-create/org-create.module.ts +++ b/console/src/app/pages/org-create/org-create.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; -import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; -import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { CreateLayoutModule } from 'src/app/modules/create-layout/create-layout.module'; diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 92a8854285..81413bafef 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { Buffer } from 'buffer'; import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs'; diff --git a/console/src/app/pages/orgs/org-members/org-members.component.html b/console/src/app/pages/orgs/org-members/org-members.component.html index 029b22cc65..02e93aef48 100644 --- a/console/src/app/pages/orgs/org-members/org-members.component.html +++ b/console/src/app/pages/orgs/org-members/org-members.component.html @@ -2,7 +2,7 @@

{{ 'ORG.MEMBER.DESCRIPTION' | translate }} - + info_outline

- diff --git a/console/src/app/pages/orgs/org-members/org-members.component.scss b/console/src/app/pages/orgs/org-members/org-members.component.scss index bdf7cb3cf8..7647171923 100644 --- a/console/src/app/pages/orgs/org-members/org-members.component.scss +++ b/console/src/app/pages/orgs/org-members/org-members.component.scss @@ -4,7 +4,7 @@ font-size: 14px; margin: -1.5rem 0 0 0; - i { + .icon { font-size: 1.2rem; height: 1.2rem; line-height: 1.2rem; diff --git a/console/src/app/pages/orgs/org-members/org-members.component.spec.ts b/console/src/app/pages/orgs/org-members/org-members.component.spec.ts index da05aa1db9..4a6238fea9 100644 --- a/console/src/app/pages/orgs/org-members/org-members.component.spec.ts +++ b/console/src/app/pages/orgs/org-members/org-members.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { OrgMembersComponent } from './org-members.component'; diff --git a/console/src/app/pages/orgs/org-members/org-members.component.ts b/console/src/app/pages/orgs/org-members/org-members.component.ts index 89c6a1d620..1c18519110 100644 --- a/console/src/app/pages/orgs/org-members/org-members.component.ts +++ b/console/src/app/pages/orgs/org-members/org-members.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter } from '@angular/core'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { LegacyPageEvent as PageEvent } from '@angular/material/legacy-paginator'; +import { MatDialog } from '@angular/material/dialog'; +import { PageEvent } from '@angular/material/paginator'; import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; import { Member } from 'src/app/proto/generated/zitadel/member_pb'; diff --git a/console/src/app/pages/orgs/org-members/org-members.module.ts b/console/src/app/pages/orgs/org-members/org-members.module.ts index 84d9ecbf05..12288ed3d7 100644 --- a/console/src/app/pages/orgs/org-members/org-members.module.ts +++ b/console/src/app/pages/orgs/org-members/org-members.module.ts @@ -1,9 +1,9 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module'; diff --git a/console/src/app/pages/orgs/org.module.ts b/console/src/app/pages/orgs/org.module.ts index d2857329fe..ae36a626ef 100644 --- a/console/src/app/pages/orgs/org.module.ts +++ b/console/src/app/pages/orgs/org.module.ts @@ -1,12 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; -import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; -import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; -import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; -import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; diff --git a/console/src/app/pages/projects/apps/additional-origins/additional-origins.component.scss b/console/src/app/pages/projects/apps/additional-origins/additional-origins.component.scss index dbc5be916a..b103818bfe 100644 --- a/console/src/app/pages/projects/apps/additional-origins/additional-origins.component.scss +++ b/console/src/app/pages/projects/apps/additional-origins/additional-origins.component.scss @@ -8,7 +8,7 @@ } button { - margin-bottom: 14px; + margin-bottom: 0.5rem; margin-right: -0.5rem; } } diff --git a/console/src/app/pages/projects/apps/app-create/app-create.component.scss b/console/src/app/pages/projects/apps/app-create/app-create.component.scss index cb2172d660..8d070f403d 100644 --- a/console/src/app/pages/projects/apps/app-create/app-create.component.scss +++ b/console/src/app/pages/projects/apps/app-create/app-create.component.scss @@ -104,7 +104,8 @@ p.desc { } .create-button { - padding: 0.5rem 4rem; + height: 3.5rem; + padding: 0 4rem; } } @@ -139,7 +140,8 @@ p.desc { .continue-button { margin-top: 3rem; display: block; - padding: 0.5rem 4rem; + height: 3.5rem; + padding: 0 4rem; } } diff --git a/console/src/app/pages/projects/apps/app-create/app-create.component.ts b/console/src/app/pages/projects/apps/app-create/app-create.component.ts index 7c9af17e78..d8e920a758 100644 --- a/console/src/app/pages/projects/apps/app-create/app-create.component.ts +++ b/console/src/app/pages/projects/apps/app-create/app-create.component.ts @@ -3,7 +3,7 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { Location } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { Buffer } from 'buffer'; import { Subject, Subscription } from 'rxjs'; diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html index e3884b6166..f624128983 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.html +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.html @@ -282,17 +282,17 @@

ClockSkew

+ + > {{ 'APP.OIDC.CLOCKSKEW' | translate }} diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss b/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss index 25409024e4..f97a914ebd 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.scss @@ -1,5 +1,3 @@ -@use '@angular/material' as mat; - @mixin app-detail-theme($theme) { $foreground: map-get($theme, foreground); $background: map-get($theme, background); @@ -247,7 +245,7 @@ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); .rt { - margin-top: 2.3rem; + margin-top: 1.7rem; margin-left: 0.5rem; } } diff --git a/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts b/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts index da77b9677a..49b5eea676 100644 --- a/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts +++ b/console/src/app/pages/projects/apps/app-detail/app-detail.component.ts @@ -2,8 +2,8 @@ import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes'; import { Location } from '@angular/common'; import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox'; -import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Buffer } from 'buffer'; @@ -268,6 +268,15 @@ export class AppDetailComponent implements OnInit, OnDestroy { if (app.app) { this.app = app.app; + // TODO: duplicates should be handled in the API + if (this.app.oidcConfig?.complianceProblemsList && this.app.oidcConfig?.complianceProblemsList.length) { + this.app.oidcConfig.complianceProblemsList = this.app.oidcConfig?.complianceProblemsList.filter( + (element, index) => { + return this.app?.oidcConfig?.complianceProblemsList.findIndex((e) => e.key === element.key) === index; + }, + ); + } + const breadcrumbs = [ new Breadcrumb({ type: BreadcrumbType.ORG, diff --git a/console/src/app/pages/projects/apps/app-detail/auth-method-dialog/auth-method-dialog.component.ts b/console/src/app/pages/projects/apps/app-detail/auth-method-dialog/auth-method-dialog.component.ts index 8b1436b716..1d7475cb04 100644 --- a/console/src/app/pages/projects/apps/app-detail/auth-method-dialog/auth-method-dialog.component.ts +++ b/console/src/app/pages/projects/apps/app-detail/auth-method-dialog/auth-method-dialog.component.ts @@ -1,8 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { - MatLegacyDialogRef as MatDialogRef, - MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, -} from '@angular/material/legacy-dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @Component({ selector: 'cnsl-auth-method-dialog', diff --git a/console/src/app/pages/projects/apps/app-secret-dialog/app-secret-dialog.component.html b/console/src/app/pages/projects/apps/app-secret-dialog/app-secret-dialog.component.html index 98973a8e21..1d68497e60 100644 --- a/console/src/app/pages/projects/apps/app-secret-dialog/app-secret-dialog.component.html +++ b/console/src/app/pages/projects/apps/app-secret-dialog/app-secret-dialog.component.html @@ -1,8 +1,9 @@

{{ 'APP.OIDC.CLIENTSECRET' | translate }}

-

{{ 'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate }}

+

{{ 'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate }}

+
ClientId: {{ data.clientId }}