Merge branch 'main' into next

This commit is contained in:
Livio Spring
2024-09-04 12:50:47 +02:00
107 changed files with 5237 additions and 225 deletions

View File

@@ -17,6 +17,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.
Actions: req.Actions,
TokenExchange: req.OidcTokenExchange,
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
}
}
@@ -30,6 +31,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
}
}
@@ -44,6 +46,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
WebKey: req.WebKey,
DebugOIDCParentError: req.DebugOidcParentError,
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
}
}
@@ -59,6 +62,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
WebKey: featureSourceToFlagPb(&f.WebKey),
DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError),
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
}
}

View File

@@ -25,6 +25,7 @@ func Test_systemFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OidcSingleV1SessionTermination: gu.Ptr(true),
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -34,6 +35,7 @@ func Test_systemFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OIDCSingleV1SessionTermination: gu.Ptr(true),
}
got := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
@@ -74,6 +76,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
},
OIDCSingleV1SessionTermination: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@@ -109,6 +115,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
Source: feature_pb.Source_SOURCE_SYSTEM,
},
OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@@ -124,6 +134,8 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
DebugOidcParentError: gu.Ptr(true),
OidcSingleV1SessionTermination: gu.Ptr(true),
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -134,6 +146,8 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
DebugOIDCParentError: gu.Ptr(true),
OIDCSingleV1SessionTermination: gu.Ptr(true),
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
@@ -178,6 +192,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelInstance,
Value: true,
},
OIDCSingleV1SessionTermination: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@@ -221,6 +239,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Enabled: false,
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
},
OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@@ -17,6 +17,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.
Actions: req.Actions,
TokenExchange: req.OidcTokenExchange,
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
}
}
@@ -30,6 +31,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
}
}
@@ -44,6 +46,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
WebKey: req.WebKey,
DebugOIDCParentError: req.DebugOidcParentError,
OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination,
}
}
@@ -59,6 +62,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
WebKey: featureSourceToFlagPb(&f.WebKey),
DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError),
OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination),
}
}

View File

@@ -25,6 +25,7 @@ func Test_systemFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OidcSingleV1SessionTermination: gu.Ptr(true),
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -34,6 +35,7 @@ func Test_systemFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
OIDCSingleV1SessionTermination: gu.Ptr(true),
}
got := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
@@ -74,6 +76,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
},
OIDCSingleV1SessionTermination: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@@ -109,6 +115,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
Source: feature_pb.Source_SOURCE_SYSTEM,
},
OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@@ -124,6 +134,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
OidcSingleV1SessionTermination: gu.Ptr(true),
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
@@ -134,6 +145,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
OIDCSingleV1SessionTermination: gu.Ptr(true),
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
@@ -178,6 +190,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelInstance,
Value: true,
},
OIDCSingleV1SessionTermination: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@@ -221,6 +237,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Enabled: false,
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
},
OidcSingleV1SessionTermination: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@@ -16,6 +16,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/handler"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
@@ -245,11 +246,20 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
}
// If there is no login client header and no id_token_hint or the id_token_hint does not have a session ID,
// do a v1 Terminate session.
// do a v1 Terminate session (which terminates all sessions of the user agent, identified by cookie).
if endSessionRequest.IDTokenHintClaims == nil || endSessionRequest.IDTokenHintClaims.SessionID == "" {
return endSessionRequest.RedirectURI, o.TerminateSession(ctx, endSessionRequest.UserID, endSessionRequest.ClientID)
}
// If the sessionID is prefixed by V1, we also terminate a v1 session.
if strings.HasPrefix(endSessionRequest.IDTokenHintClaims.SessionID, handler.IDPrefixV1) {
err = o.terminateV1Session(ctx, endSessionRequest.UserID, endSessionRequest.IDTokenHintClaims.SessionID)
if err != nil {
return "", err
}
return endSessionRequest.RedirectURI, nil
}
// terminate the v2 session of the id_token_hint
_, err = o.command.TerminateSessionWithoutTokenCheck(ctx, endSessionRequest.IDTokenHintClaims.SessionID)
if err != nil {
@@ -258,6 +268,30 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
return endSessionRequest.RedirectURI, nil
}
// terminateV1Session terminates "v1" sessions created through the login UI.
// Depending on the flag, we either terminate a single session or all of the user agent
func (o *OPStorage) terminateV1Session(ctx context.Context, userID, sessionID string) error {
ctx = authz.SetCtxData(ctx, authz.CtxData{UserID: userID})
// if the flag is active we only terminate the specific session
if authz.GetFeatures(ctx).OIDCSingleV1SessionTermination {
userAgentID, err := o.repo.UserAgentIDBySessionID(ctx, sessionID)
if err != nil {
return err
}
return o.command.HumansSignOut(ctx, userAgentID, []string{userID})
}
// otherwise we search for all active sessions within the same user agent of the current session id
userAgentID, userIDs, err := o.repo.ActiveUserIDsBySessionID(ctx, sessionID)
if err != nil {
logging.WithError(err).Error("error retrieving user sessions")
return err
}
if len(userIDs) == 0 {
return nil
}
return o.command.HumansSignOut(ctx, userAgentID, userIDs)
}
func (o *OPStorage) RevokeToken(ctx context.Context, token, userID, clientID string) (err *oidc.Error) {
ctx, span := tracing.NewSpan(ctx)
defer func() {
@@ -564,6 +598,7 @@ func (s *Server) authResponseToken(authReq *AuthRequest, authorizer op.Authorize
domain.TokenReasonAuthRequest,
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),
authReq.SessionID,
)
if err != nil {
op.AuthRequestError(w, r, authReq, err, authorizer)

View File

@@ -45,6 +45,7 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ
domain.TokenReasonClientCredentials,
nil,
false,
"",
)
if err != nil {
return nil, err

View File

@@ -85,6 +85,7 @@ func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.A
domain.TokenReasonAuthRequest,
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),
authReq.SessionID,
)
if err != nil {
return nil, err

View File

@@ -298,6 +298,7 @@ func (s *Server) createExchangeAccessToken(
reason,
actor,
slices.Contains(scope, oidc.ScopeOfflineAccess),
"",
)
if err != nil {
return "", "", "", 0, err
@@ -342,6 +343,7 @@ func (s *Server) createExchangeJWT(
reason,
actor,
slices.Contains(scope, oidc.ScopeOfflineAccess),
"",
)
accessToken, err = s.createJWT(ctx, client, session, getUserInfo, roleAssertion, getSigner)
if err != nil {

View File

@@ -53,6 +53,7 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr
domain.TokenReasonJWTProfile,
nil,
false,
"",
)
if err != nil {
return nil, err

View File

@@ -67,6 +67,7 @@ func (s *Server) refreshTokenV1(ctx context.Context, client *Client, r *op.Clien
domain.TokenReasonRefresh,
refreshToken.Actor,
true,
"",
)
if err != nil {
return nil, err

View File

@@ -162,7 +162,7 @@ func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) {
action := mux.Vars(r)["action"]
switch action {
case deviceAuthAllowed:
_, err = l.command.ApproveDeviceAuth(r.Context(), authDev.DeviceCode, authReq.UserID, authReq.UserOrgID, authReq.UserAuthMethodTypes(), authReq.AuthTime, authReq.PreferredLanguage, authReq.ToUserAgent())
_, err = l.command.ApproveDeviceAuth(r.Context(), authDev.DeviceCode, authReq.UserID, authReq.UserOrgID, authReq.UserAuthMethodTypes(), authReq.AuthTime, authReq.PreferredLanguage, authReq.ToUserAgent(), authReq.SessionID)
case deviceAuthDenied:
_, err = l.command.CancelDeviceAuth(r.Context(), authDev.DeviceCode, domain.DeviceAuthCanceledDenied)
default:

View File

@@ -250,6 +250,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Пол
Female: Женски пол
Male: Мъжки
@@ -289,6 +290,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Правила и условия
TosConfirm: Приемам
TosLinkText: TOS
@@ -357,6 +359,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Упълномощаване на устройството
UserCode:

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Pohlaví
Female: Žena
Male: Muž
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Obchodní podmínky
TosConfirm: Souhlasím s
TosLinkText: obchodními podmínkami
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Autorizace zařízení
UserCode:

View File

@@ -252,6 +252,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Geschlecht
Female: weiblich
Male: männlich
@@ -292,6 +293,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGB
@@ -366,6 +368,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Gerät verbinden
UserCode:

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Gender
Female: Female
Male: Male
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Device Authorization
UserCode:

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Género
Female: Mujer
Male: Hombre
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Términos y condiciones
TosConfirm: Acepto los
TosLinkText: TDS
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
Footer:
PoweredBy: Powered By

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Genre
Female: Femme
Male: Homme
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Termes et conditions
TosConfirm: J'accepte les
TosLinkText: TOS
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Autorisation de l'appareil

View File

@@ -0,0 +1,466 @@
Login:
Title: Selamat Datang kembali!
Description: Masukkan data masuk Anda.
TitleLinking: Login untuk menghubungkan pengguna
DescriptionLinking: Masukkan data login Anda untuk menghubungkan pengguna eksternal Anda.
LoginNameLabel: Nama Masuk
UsernamePlaceHolder: nama belakang
LoginnamePlaceHolder: nama pengguna@domain
ExternalUserDescription: Masuk dengan pengguna eksternal.
MustBeMemberOfOrg: 'Pengguna harus menjadi anggota {{.OrgName}} organisasi.'
RegisterButtonText: Daftar
NextButtonText: Berikutnya
LDAP:
Title: Login
Description: Masukkan data masuk Anda.
LoginNameLabel: Nama Masuk
PasswordLabel: Kata sandi
NextButtonText: Berikutnya
SelectAccount:
Title: Pilih Akun
Description: Gunakan akun Anda
TitleLinking: Pilih akun untuk penautan pengguna
DescriptionLinking: Pilih akun Anda untuk ditautkan dengan pengguna eksternal Anda.
OtherUser: Pengguna Lain
SessionState0: aktif
SessionState1: Keluar
MustBeMemberOfOrg: 'Pengguna harus menjadi anggota {{.OrgName}} organisasi.'
Password:
Title: Kata sandi
Description: Masukkan data masuk Anda.
PasswordLabel: Kata sandi
MinLength: Setidaknya harus begitu
MinLengthp2: karakter panjang.
MaxLength: Panjangnya harus kurang dari 70 karakter.
HasUppercase: Harus menyertakan huruf besar.
HasLowercase: Harus menyertakan huruf kecil.
HasNumber: Harus menyertakan nomor.
HasSymbol: Harus menyertakan simbol.
Confirmation: Konfirmasi kata sandi cocok.
ResetLinkText: Atur Ulang Kata Sandi
BackButtonText: Kembali
NextButtonText: Berikutnya
UsernameChange:
Title: Ubah Nama Pengguna
Description: Tetapkan nama pengguna baru Anda
UsernameLabel: Nama belakang
CancelButtonText: Membatalkan
NextButtonText: Berikutnya
UsernameChangeDone:
Title: Nama Pengguna Berubah
Description: Nama pengguna Anda berhasil diubah.
NextButtonText: Berikutnya
InitPassword:
Title: Tetapkan Kata Sandi
Description: Anda telah menerima kode, yang harus Anda masukkan pada formulir di bawah ini, untuk mengatur kata sandi baru Anda.
CodeLabel: Kode
NewPasswordLabel: Kata Sandi Baru
NewPasswordConfirmLabel: Konfirmasi Kata Sandi
ResendButtonText: Kirim Ulang Kode
NextButtonText: Berikutnya
InitPasswordDone:
Title: Kumpulan Kata Sandi
Description: Kata sandi berhasil disetel
NextButtonText: Berikutnya
CancelButtonText: Membatalkan
InitUser:
Title: Aktifkan Pengguna
Description: Verifikasi email Anda dengan kode di bawah ini dan atur kata sandi Anda.
CodeLabel: Kode
NewPasswordLabel: Kata Sandi Baru
NewPasswordConfirm: Konfirmasi Kata Sandi
NextButtonText: Berikutnya
ResendButtonText: Kirim Ulang Kode
InitUserDone:
Title: Pengguna Diaktifkan
Description: Email terverifikasi dan Kata Sandi berhasil ditetapkan
NextButtonText: Berikutnya
CancelButtonText: Membatalkan
InitMFAPrompt:
Title: Pengaturan 2 Faktor
Description: Otentikasi 2 faktor memberi Anda keamanan tambahan untuk akun pengguna Anda.
Provider0: 'Aplikasi Authenticator (misalnya Google/Microsoft Authenticator, Authy)'
Provider1: 'Tergantung pada perangkat (misalnya FaceID, Windows Hello, Fingerprint)'
Provider3: SMS OTP
Provider4: Email OTP
NextButtonText: Berikutnya
SkipButtonText: Melewati
InitMFAOTP:
Title: Verifikasi 2 Faktor
Description: 'Buat 2 faktor Anda. '
OTPDescription: Pindai kode dengan aplikasi autentikator Anda (misalnya Google/Microsoft Authenticator, Authy) atau salin rahasianya dan masukkan kode yang dihasilkan di bawah.
SecretLabel: Rahasia
CodeLabel: Kode
NextButtonText: Berikutnya
CancelButtonText: Membatalkan
InitMFAOTPSMS:
Title: Verifikasi 2 Faktor
DescriptionPhone: 'Buat 2 faktor Anda. '
DescriptionCode: 'Buat 2 faktor Anda. '
PhoneLabel: Telepon
CodeLabel: Kode
EditButtonText: Sunting
ResendButtonText: Kirim Ulang Kode
NextButtonText: Berikutnya
InitMFAU2F:
Title: Tambahkan Kunci Keamanan
Description: Kunci keamanan adalah metode verifikasi yang dapat dipasang di ponsel Anda, menggunakan Bluetooth, atau dicolokkan langsung ke port USB komputer Anda.
TokenNameLabel: Nama kunci keamanan/perangkat
NotSupported: 'WebAuthN tidak didukung oleh browser Anda. '
RegisterTokenButtonText: Tambahkan kunci keamanan
ErrorRetry: 'Coba lagi, buat tantangan baru atau pilih metode lain.'
InitMFADone:
Title: Terverifikasi 2 faktor
Description: 'Luar biasa! '
NextButtonText: Berikutnya
CancelButtonText: Membatalkan
MFAProvider:
Provider0: 'Aplikasi Authenticator (misalnya Google/Microsoft Authenticator, Authy)'
Provider1: 'Tergantung pada perangkat (misalnya FaceID, Windows Hello, Fingerprint)'
Provider3: SMS OTP
Provider4: Email OTP
ChooseOther: atau pilih opsi lain
VerifyMFAOTP:
Title: Verifikasi 2 Faktor
Description: Verifikasi faktor kedua Anda
CodeLabel: Kode
NextButtonText: Berikutnya
VerifyOTP:
Title: Verifikasi 2 Faktor
Description: Verifikasi faktor kedua Anda
CodeLabel: Kode
ResendButtonText: Kirim Ulang Kode
NextButtonText: Berikutnya
VerifyMFAU2F:
Title: Verifikasi 2 Faktor
Description: Verifikasi 2-Faktor Anda dengan perangkat yang terdaftar (misalnya FaceID, Windows Hello, Fingerprint)
NotSupported: 'WebAuthN tidak didukung oleh browser Anda. '
ErrorRetry: 'Coba lagi, buat permintaan baru atau pilih metode lain.'
ValidateTokenButtonText: Verifikasi 2 Faktor
Passwordless:
Title: Masuk Tanpa Kata Sandi
Description: Masuk dengan metode autentikasi yang disediakan oleh perangkat Anda seperti FaceID, Windows Hello, atau Sidik Jari.
NotSupported: 'WebAuthN tidak didukung oleh browser Anda. '
ErrorRetry: 'Coba lagi, buat tantangan baru atau pilih metode lain.'
LoginWithPwButtonText: Masuk dengan kata sandi
ValidateTokenButtonText: Masuk dengan tanpa kata sandi
PasswordlessPrompt:
Title: Pengaturan Tanpa Kata Sandi
Description: 'Apakah Anda ingin mengatur login tanpa kata sandi? '
DescriptionInit: 'Anda perlu mengatur login tanpa kata sandi. '
PasswordlessButtonText: Tanpa kata sandi
NextButtonText: Berikutnya
SkipButtonText: Melewati
PasswordlessRegistration:
Title: Pengaturan Tanpa Kata Sandi
Description: Tambahkan otentikasi Anda dengan memberikan nama (misalnya MyMobilePhone, MacBook, dll) dan kemudian mengklik tombol 'Daftar tanpa kata sandi' di bawah.
TokenNameLabel: Nama perangkat
NotSupported: 'WebAuthN tidak didukung oleh browser Anda. '
RegisterTokenButtonText: Daftar tanpa kata sandi
ErrorRetry: 'Coba lagi, buat tantangan baru atau pilih metode lain.'
PasswordlessRegistrationDone:
Title: Pengaturan Tanpa Kata Sandi
Description: Perangkat tanpa kata sandi berhasil ditambahkan.
DescriptionClose: Anda sekarang dapat menutup jendela ini.
NextButtonText: Berikutnya
CancelButtonText: Membatalkan
PasswordChange:
Title: Ubah Kata Sandi
Description: 'Ubah kata sandi Anda. '
ExpiredDescription: 'Kata sandi Anda telah kedaluwarsa dan harus diubah. '
OldPasswordLabel: Kata Sandi Lama
NewPasswordLabel: Kata Sandi Baru
NewPasswordConfirmLabel: Konfirmasi kata sandi
CancelButtonText: Membatalkan
NextButtonText: Berikutnya
Footer: catatan kaki
PasswordChangeDone:
Title: Ubah Kata Sandi
Description: Kata sandi Anda berhasil diubah.
NextButtonText: Berikutnya
PasswordResetDone:
Title: Tautan Reset Kata Sandi Terkirim
Description: Periksa email Anda untuk mengatur ulang kata sandi Anda.
NextButtonText: Berikutnya
EmailVerification:
Title: Verifikasi Email
Description: 'Kami telah mengirimi Anda email untuk memverifikasi alamat Anda. '
CodeLabel: Kode
NextButtonText: Berikutnya
ResendButtonText: Kirim Ulang Kode
EmailVerificationDone:
Title: Verifikasi Email
Description: Alamat email Anda telah berhasil diverifikasi.
NextButtonText: Berikutnya
CancelButtonText: Membatalkan
LoginButtonText: Login
RegisterOption:
Title: Opsi Pendaftaran
Description: Pilih bagaimana Anda ingin mendaftar
RegisterUsernamePasswordButtonText: Dengan nama pengguna dan kata sandi
ExternalLoginDescription: atau mendaftar dengan pengguna eksternal
LoginButtonText: Login
RegistrationUser:
Title: Pendaftaran
Description: 'Masukkan Data Pengguna Anda. '
DescriptionOrgRegister: Masukkan Data Pengguna Anda.
EmailLabel: E-mail
UsernameLabel: Nama belakang
FirstnameLabel: Nama yang diberikan
LastnameLabel: Nama keluarga
LanguageLabel: Bahasa
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
Bulgarian: Български
Portuguese: Português
Macedonian: Македонски
Czech: Čeština
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Jenis kelamin
Female: Perempuan
Male: Pria
Diverse: beragam / X
PasswordLabel: Kata sandi
PasswordConfirmLabel: Konfirmasi kata sandi
TosAndPrivacyLabel: Syarat dan Ketentuan
TosConfirm: Saya menerima itu
TosLinkText: KL
PrivacyConfirm: Saya menerima itu
PrivacyLinkText: kebijakan privasi
ExternalLogin: atau mendaftar dengan pengguna eksternal
BackButtonText: Login
NextButtonText: Berikutnya
ExternalRegistrationUserOverview:
Title: Registrasi Pengguna Eksternal
Description: 'Kami telah mengambil detail pengguna Anda dari penyedia yang dipilih. '
EmailLabel: E-mail
UsernameLabel: Nama belakang
FirstnameLabel: Nama yang diberikan
LastnameLabel: Nama keluarga
NicknameLabel: Nama panggilan
PhoneLabel: Nomor telepon
LanguageLabel: Bahasa
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
Bulgarian: Български
Portuguese: Português
Macedonian: Македонски
Czech: Čeština
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Syarat dan Ketentuan
TosConfirm: Saya menerima itu
TosLinkText: KL
PrivacyConfirm: Saya menerima itu
PrivacyLinkText: kebijakan privasi
ExternalLogin: atau mendaftar dengan pengguna eksternal
BackButtonText: tidak
NextButtonText: Menyimpan
RegistrationOrg:
Title: Pendaftaran Organisasi
Description: Masukkan nama organisasi dan data pengguna Anda.
OrgNameLabel: Nama organisasi
EmailLabel: E-mail
UsernameLabel: Nama belakang
FirstnameLabel: Nama yang diberikan
LastnameLabel: Nama keluarga
PasswordLabel: Kata sandi
PasswordConfirmLabel: Konfirmasi kata sandi
TosAndPrivacyLabel: Syarat dan Ketentuan
TosConfirm: Saya menerima itu
TosLinkText: KL
PrivacyConfirm: Saya menerima itu
PrivacyLinkText: kebijakan privasi
SaveButtonText: Buat organisasi
LoginSuccess:
Title: Masuk Berhasil
AutoRedirectDescription: 'Anda akan diarahkan kembali ke aplikasi Anda secara otomatis. '
RedirectedDescription: Anda sekarang dapat menutup jendela ini.
NextButtonText: Berikutnya
LogoutDone:
Title: Keluar
Description: Anda telah berhasil logout.
LoginButtonText: Login
LinkingUserPrompt:
Title: Pengguna Lama Ditemukan
Description: 'Apakah Anda ingin menautkan akun Anda yang ada:'
LinkButtonText: Link
OtherButtonText: Pilihan lain
LinkingUsersDone:
Title: Menghubungkan Pengguna
Description: Tertaut pengguna.
CancelButtonText: Membatalkan
NextButtonText: Berikutnya
ExternalNotFound:
Title: Pengguna Eksternal Tidak Ditemukan
Description: 'Pengguna eksternal tidak ditemukan. '
LinkButtonText: Link
AutoRegisterButtonText: Daftar
TosAndPrivacyLabel: Syarat dan Ketentuan
TosConfirm: Saya menerima itu
TosLinkText: KL
PrivacyConfirm: Saya menerima itu
PrivacyLinkText: kebijakan privasi
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
Bulgarian: Български
Portuguese: Português
Macedonian: Македонски
Czech: Čeština
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Otorisasi Perangkat
UserCode:
Label: Kode Pengguna
Description: Masukkan kode pengguna yang disajikan pada perangkat.
ButtonNext: Berikutnya
Action:
Description: Berikan akses perangkat.
GrantDevice: Anda akan memberikan perangkat
AccessToScopes: akses ke cakupan berikut
Button:
Allow: Mengizinkan
Deny: Membantah
Done:
Description: Selesai.
Approved: 'Otorisasi perangkat disetujui. '
Denied: 'Otorisasi perangkat ditolak. '
Footer:
PoweredBy: Didukung oleh
Tos: KL
PrivacyPolicy: Kebijakan privasi
Help: Membantu
SupportEmail: Email Dukungan
SignIn: 'Masuk dengan {{.Provider}}'
Errors:
Internal: Terjadi kesalahan internal
AuthRequest:
NotFound: Tidak dapat menemukan permintaan autentikasi
UserAgentNotCorresponding: Agen Pengguna tidak sesuai
UserAgentNotFound: ID Agen Pengguna tidak ditemukan
TokenNotFound: Token tidak ditemukan
RequestTypeNotSupported: Jenis permintaan tidak didukung
MissingParameters: Parameter yang diperlukan tidak ada
User:
NotFound: Pengguna tidak dapat ditemukan
AlreadyExists: Pengguna sudah ada
Inactive: Pengguna tidak aktif
NotFoundOnOrg: Pengguna tidak dapat ditemukan di organisasi yang dipilih
NotAllowedOrg: Pengguna bukan anggota organisasi yang diperlukan
NotMatchingUserID: Pengguna dan pengguna dalam permintaan authrequest tidak cocok
UserIDMissing: ID Pengguna kosong
Invalid: Data pengguna tidak valid
DomainNotAllowedAsUsername: Domain sudah dipesan dan tidak dapat digunakan
NotAllowedToLink: Pengguna tidak diperbolehkan terhubung dengan penyedia login eksternal
Profile:
NotFound: Profil tidak ditemukan
NotChanged: Profil tidak berubah
Empty: Profil kosong
FirstNameEmpty: Nama depan di profil kosong
LastNameEmpty: Nama keluarga di profil kosong
IDMissing: ID Profil tidak ada
Email:
NotFound: Email tidak ditemukan
Invalid: Email tidak valid
AlreadyVerified: Email sudah diverifikasi
NotChanged: Email tidak diubah
Empty: Emailnya kosong
IDMissing: ID email tidak ada
Phone:
NotFound: Telepon tidak ditemukan
Invalid: Telepon tidak valid
AlreadyVerified: Telepon sudah diverifikasi
Empty: Telepon kosong
NotChanged: Telepon tidak berubah
Address:
NotFound: Alamat tidak ditemukan
NotChanged: Alamat tidak diubah
Username:
AlreadyExists: Nama pengguna sudah dipakai
Reserved: Nama pengguna sudah dipakai
Empty: Nama pengguna kosong
Password:
ConfirmationWrong: Konfirmasi kata sandi salah
Empty: Kata sandi kosong
Invalid: Kata sandi tidak valid
InvalidAndLocked: Kata sandi tidak valid dan pengguna terkunci, hubungi administrator Anda.
NotChanged: Kata sandi baru tidak boleh sama dengan kata sandi Anda saat ini
UsernameOrPassword:
Invalid: Nama Pengguna atau Kata Sandi tidak valid
PasswordComplexityPolicy:
NotFound: Kebijakan kata sandi tidak ditemukan
MinLength: Kata sandi terlalu pendek
HasLower: Kata sandi harus mengandung huruf kecil
HasUpper: Kata sandi harus mengandung huruf besar
HasNumber: Kata sandi harus berisi nomor
HasSymbol: Kata sandi harus mengandung simbol
Code:
Expired: Kode sudah habis masa berlakunya
Invalid: Kode tidak valid
Empty: Kode kosong
CryptoCodeNil: Kode kripto nihil
NotFound: Tidak dapat menemukan kode
GeneratorAlgNotSupported: Algoritme generator tidak didukung
EmailVerify:
UserIDEmpty: ID Pengguna kosong
ExternalData:
CouldNotRead: Data eksternal tidak dapat dibaca dengan benar
MFA:
NoProviders: Tidak ada penyedia multifaktor yang tersedia
OTP:
AlreadyReady: OTP multifaktor (OneTimePassword) sudah disiapkan
NotExisting: OTP multifaktor (OneTimePassword) tidak ada
InvalidCode: Kode tidak valid
NotReady: OTP multifaktor (OneTimePassword) belum siap
Locked: Pengguna terkunci
SomethingWentWrong: Ada yang tidak beres
NotActive: Pengguna tidak aktif
ExternalIDP:
IDPTypeNotImplemented: Tipe IDP tidak diterapkan
NotAllowed: Penyedia Login Eksternal tidak diizinkan
IDPConfigIDEmpty: ID Penyedia Identitas kosong
ExternalUserIDEmpty: ID Pengguna Eksternal kosong
UserDisplayNameEmpty: Nama Tampilan Pengguna kosong
NoExternalUserData: Tidak ada Data Pengguna eksternal yang diterima
CreationNotAllowed: Pembuatan pengguna baru tidak diperbolehkan pada penyedia ini
LinkingNotAllowed: Menautkan pengguna tidak diperbolehkan di penyedia ini
NoOptionAllowed: 'Pembuatan tautan tidak diperbolehkan pada penyedia ini. '
GrantRequired: 'Masuk tidak dapat dilakukan. '
ProjectRequired: 'Masuk tidak dapat dilakukan. '
IdentityProvider:
InvalidConfig: Konfigurasi Penyedia Identitas tidak valid
IAM:
LockoutPolicy:
NotExisting: Kebijakan Lockout tidak ada
Org:
LoginPolicy:
RegistrationNotAllowed: Pendaftaran tidak diperbolehkan
DeviceAuth:
NotExisting: Kode Pengguna tidak ada
optional: (opsional)

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Genere
Female: Femminile
Male: Maschile
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Autorizzazione del dispositivo

View File

@@ -245,6 +245,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: 性別
Female: 女性
Male: 男性
@@ -285,6 +286,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: 利用規約
TosConfirm: 私は利用規約を承諾します。
TosLinkText: TOS
@@ -359,6 +361,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: デバイス認証

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Пол
Female: Женски
Male: Машки
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Правила и услови
TosConfirm: Се согласувам со
TosLinkText: правилата за користење
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Овластување преку уред

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Geslacht
Female: Vrouw
Male: Man
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Nederlands: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Algemene voorwaarden
TosConfirm: Ik accepteer de
TosLinkText: AV
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Apparaat Autorisatie
UserCode:

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Płeć
Female: Kobieta
Male: Mężczyzna
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Warunki i zasady
TosConfirm: Akceptuję
TosLinkText: Warunki korzystania
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Autoryzacja urządzenia

View File

@@ -249,6 +249,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Gênero
Female: Feminino
Male: Masculino
@@ -289,6 +290,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Termos e condições
TosConfirm: Eu aceito os
TosLinkText: termos de serviço
@@ -363,6 +365,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Autorização de dispositivo

View File

@@ -252,6 +252,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Пол
Female: Женский
Male: Мужской
@@ -292,6 +293,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Условия использования
TosConfirm: Я согласен с
TosLinkText: Пользовательским соглашением
@@ -366,6 +368,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Авторизация устройства

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: Kön
Female: Man
Male: Kvinna
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: Användarvillkor
TosConfirm: Jag accepterar
TosLinkText: Användarvillkoren
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: Tillgång från hårdvaruenhet
UserCode:

View File

@@ -253,6 +253,7 @@ RegistrationUser:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
GenderLabel: 性别
Female: 女性
Male: 男性
@@ -293,6 +294,7 @@ ExternalRegistrationUserOverview:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
TosAndPrivacyLabel: 条款和条款
TosConfirm: 我接受
TosLinkText: 服务条款
@@ -367,6 +369,7 @@ ExternalNotFound:
Russian: Русский
Dutch: Nederlands
Swedish: Svenska
Indonesian: Bahasa Indonesia
DeviceAuth:
Title: 设备授权
UserCode:

View File

@@ -72,6 +72,8 @@
</option>
<option value="fr" id="fr" {{if (selectedLanguage "fr")}} selected {{end}}>{{t "ExternalNotFound.French"}}
</option>
<option value="id" id="id" {{if (selectedLanguage "id")}} selected {{end}}>{{t "ExternalNotFound.Indonesian"}}
</option>
<option value="it" id="it" {{if (selectedLanguage "it")}} selected {{end}}>{{t "ExternalNotFound.Italian"}}
</option>
<option value="ja" id="ja" {{if (selectedLanguage "ja")}} selected {{end}}>{{t "ExternalNotFound.Japanese"}}

View File

@@ -74,8 +74,8 @@ type privacyPolicyProvider interface {
}
type userSessionViewProvider interface {
UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error)
UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error)
UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error)
UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error)
GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*query.CurrentState, error)
}
@@ -1048,6 +1048,7 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
if err != nil {
return nil, err
}
request.SessionID = userSession.ID
request.DisplayName = userSession.DisplayName
request.AvatarKey = userSession.AvatarKey
if user.HumanView != nil && user.HumanView.PreferredLanguage != "" {
@@ -1532,7 +1533,7 @@ func userSessionsByUserAgentID(ctx context.Context, provider userSessionViewProv
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
session, err := provider.UserSessionsByAgentID(agentID, instanceID)
session, err := provider.UserSessionsByAgentID(ctx, agentID, instanceID)
if err != nil {
return nil, err
}
@@ -1572,7 +1573,7 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve
OnError(err).
Errorf("could not get current sequence for userSessionByIDs")
session, err := provider.UserSessionByIDs(agentID, user.ID, instanceID)
session, err := provider.UserSessionByIDs(ctx, agentID, user.ID, instanceID)
if err != nil {
if !zerrors.IsNotFound(err) {
return nil, err

View File

@@ -34,11 +34,11 @@ var (
type mockViewNoUserSession struct{}
func (m *mockViewNoUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) {
func (m *mockViewNoUserSession) UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error) {
return nil, zerrors.ThrowNotFound(nil, "id", "user session not found")
}
func (m *mockViewNoUserSession) UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) {
func (m *mockViewNoUserSession) UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error) {
return nil, nil
}
@@ -48,11 +48,11 @@ func (m *mockViewNoUserSession) GetLatestUserSessionSequence(ctx context.Context
type mockViewErrUserSession struct{}
func (m *mockViewErrUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) {
func (m *mockViewErrUserSession) UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error) {
return nil, zerrors.ThrowInternal(nil, "id", "internal error")
}
func (m *mockViewErrUserSession) UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) {
func (m *mockViewErrUserSession) UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error) {
return nil, zerrors.ThrowInternal(nil, "id", "internal error")
}
@@ -76,7 +76,7 @@ type mockUser struct {
SessionState domain.UserSessionState
}
func (m *mockViewUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) {
func (m *mockViewUserSession) UserSessionByIDs(context.Context, string, string, string) (*user_view_model.UserSessionView, error) {
return &user_view_model.UserSessionView{
ExternalLoginVerification: sql.NullTime{Time: m.ExternalLoginVerification},
PasswordlessVerification: sql.NullTime{Time: m.PasswordlessVerification},
@@ -86,7 +86,7 @@ func (m *mockViewUserSession) UserSessionByIDs(string, string, string) (*user_vi
}, nil
}
func (m *mockViewUserSession) UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) {
func (m *mockViewUserSession) UserSessionsByAgentID(context.Context, string, string) ([]*user_view_model.UserSessionView, error) {
sessions := make([]*user_view_model.UserSessionView, len(m.Users))
for i, user := range m.Users {
sessions[i] = &user_view_model.UserSessionView{

View File

@@ -28,7 +28,7 @@ func (repo *UserRepo) Health(ctx context.Context) error {
}
func (repo *UserRepo) UserSessionUserIDsByAgentID(ctx context.Context, agentID string) ([]string, error) {
userSessions, err := repo.View.UserSessionsByAgentID(agentID, authz.GetInstance(ctx).InstanceID())
userSessions, err := repo.View.UserSessionsByAgentID(ctx, agentID, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
@@ -41,6 +41,14 @@ func (repo *UserRepo) UserSessionUserIDsByAgentID(ctx context.Context, agentID s
return userIDs, nil
}
func (repo *UserRepo) UserAgentIDBySessionID(ctx context.Context, sessionID string) (string, error) {
return repo.View.UserAgentIDBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID())
}
func (repo *UserRepo) ActiveUserIDsBySessionID(ctx context.Context, sessionID string) (userAgentID string, userIDs []string, err error) {
return repo.View.ActiveUserIDsBySessionID(ctx, sessionID, authz.GetInstance(ctx).InstanceID())
}
func (repo *UserRepo) UserEventsByID(ctx context.Context, id string, changeDate time.Time, eventTypes []eventstore.EventType) ([]eventstore.Event, error) {
query, err := usr_view.UserByIDQuery(id, authz.GetInstance(ctx).InstanceID(), changeDate, eventTypes)
if err != nil {

View File

@@ -14,7 +14,7 @@ type UserSessionRepo struct {
}
func (repo *UserSessionRepo) GetMyUserSessions(ctx context.Context) ([]*usr_model.UserSessionView, error) {
userSessions, err := repo.View.UserSessionsByAgentID(authz.GetCtxData(ctx).AgentID, authz.GetInstance(ctx).InstanceID())
userSessions, err := repo.View.UserSessionsByAgentID(ctx, authz.GetCtxData(ctx).AgentID, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
handler2 "github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/id"
query2 "github.com/zitadel/zitadel/internal/query"
)
@@ -40,6 +41,7 @@ func Register(ctx context.Context, configs Config, view *view.View, queries *que
configs.overwrite("UserSession"),
view,
queries,
id.SonyFlakeGenerator(),
))
projections = append(projections, newToken(ctx,

View File

@@ -2,12 +2,14 @@ package handler
import (
"context"
"slices"
"time"
auth_view "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/id"
query2 "github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
@@ -18,12 +20,15 @@ import (
const (
userSessionTable = "auth.user_sessions"
IDPrefixV1 = "V1_"
)
type UserSession struct {
queries *query2.Queries
view *auth_view.View
es handler.EventStore
queries *query2.Queries
view *auth_view.View
es handler.EventStore
idGenerator id.Generator
}
var _ handler.Projection = (*UserSession)(nil)
@@ -33,14 +38,16 @@ func newUserSession(
config handler.Config,
view *auth_view.View,
queries *query2.Queries,
idGenerator id.Generator,
) *handler.Handler {
return handler.NewHandler(
ctx,
&config,
&UserSession{
queries: queries,
view: view,
es: config.Eventstore,
queries: queries,
view: view,
es: config.Eventstore,
idGenerator: idGenerator,
},
)
}
@@ -187,7 +194,7 @@ func (s *UserSession) Reducers() []handler.AggregateReducer {
}
}
func sessionColumns(event eventstore.Event, columns ...handler.Column) ([]handler.Column, error) {
func (u *UserSession) sessionColumns(event eventstore.Event, columns ...handler.Column) ([]handler.Column, error) {
userAgent, err := agentIDFromSession(event)
if err != nil {
return nil, err
@@ -203,14 +210,34 @@ func sessionColumns(event eventstore.Event, columns ...handler.Column) ([]handle
}, columns...), nil
}
func (u *UserSession) sessionColumnsActivate(event eventstore.Event, columns ...handler.Column) ([]handler.Column, error) {
sessionID, err := u.idGenerator.Next()
if err != nil {
return nil, err
}
sessionID = IDPrefixV1 + sessionID
columns = slices.Grow(columns, 2)
columns = append(columns,
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
handler.NewCol(view_model.UserSessionKeyID,
handler.OnlySetValueInCase(userSessionTable, sessionID,
handler.ConditionOr(
handler.ColumnChangedCondition(userSessionTable, view_model.UserSessionKeyState, domain.UserSessionStateTerminated, domain.UserSessionStateActive),
handler.ColumnIsNullCondition(userSessionTable, view_model.UserSessionKeyID),
),
),
),
)
return u.sessionColumns(event, columns...)
}
func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err error) {
// in case anything needs to be change here check if appendEvent function needs the change as well
switch event.Type() {
case user.UserV1PasswordCheckSucceededType,
user.HumanPasswordCheckSucceededType:
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeyPasswordVerification, event.CreatedAt()),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -218,9 +245,8 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
return handler.NewUpsertStatement(event, columns[0:3], columns), nil
case user.UserV1PasswordCheckFailedType,
user.HumanPasswordCheckFailedType:
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeyPasswordVerification, time.Time{}),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -228,10 +254,9 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
return handler.NewUpsertStatement(event, columns[0:3], columns), nil
case user.UserV1MFAOTPCheckSucceededType,
user.HumanMFAOTPCheckSucceededType:
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeySecondFactorVerification, event.CreatedAt()),
handler.NewCol(view_model.UserSessionKeySecondFactorVerificationType, domain.MFATypeTOTP),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -240,9 +265,8 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
case user.UserV1MFAOTPCheckFailedType,
user.HumanMFAOTPCheckFailedType,
user.HumanU2FTokenCheckFailedType:
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeySecondFactorVerification, time.Time{}),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -250,7 +274,7 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
return handler.NewUpsertStatement(event, columns[0:3], columns), nil
case user.UserV1SignedOutType,
user.HumanSignedOutType:
columns, err := sessionColumns(event,
columns, err := u.sessionColumns(event,
handler.NewCol(view_model.UserSessionKeyPasswordlessVerification, time.Time{}),
handler.NewCol(view_model.UserSessionKeyPasswordVerification, time.Time{}),
handler.NewCol(view_model.UserSessionKeySecondFactorVerification, time.Time{}),
@@ -270,10 +294,9 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
if err != nil {
return nil, err
}
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeyExternalLoginVerification, event.CreatedAt()),
handler.NewCol(view_model.UserSessionKeySelectedIDPConfigID, data.SelectedIDPConfigID),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -285,10 +308,9 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
if err != nil {
return nil, err
}
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeySecondFactorVerification, event.CreatedAt()),
handler.NewCol(view_model.UserSessionKeySecondFactorVerificationType, domain.MFATypeU2F),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -300,11 +322,10 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
if err != nil {
return nil, err
}
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeyPasswordlessVerification, event.CreatedAt()),
handler.NewCol(view_model.UserSessionKeyMultiFactorVerification, event.CreatedAt()),
handler.NewCol(view_model.UserSessionKeyMultiFactorVerificationType, domain.MFATypeU2FUserVerification),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -316,10 +337,9 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
if err != nil {
return nil, err
}
columns, err := sessionColumns(event,
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeyPasswordlessVerification, time.Time{}),
handler.NewCol(view_model.UserSessionKeyMultiFactorVerification, time.Time{}),
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
)
if err != nil {
return nil, err
@@ -429,8 +449,7 @@ func (u *UserSession) Reduce(event eventstore.Event) (_ *handler.Statement, err
},
), nil
case user.HumanRegisteredType:
columns, err := sessionColumns(event,
handler.NewCol(view_model.UserSessionKeyState, domain.UserSessionStateActive),
columns, err := u.sessionColumnsActivate(event,
handler.NewCol(view_model.UserSessionKeyPasswordVerification, event.CreatedAt()),
)
if err != nil {

View File

@@ -12,12 +12,20 @@ const (
userSessionTable = "auth.user_sessions"
)
func (v *View) UserSessionByIDs(agentID, userID, instanceID string) (*model.UserSessionView, error) {
return view.UserSessionByIDs(v.client, agentID, userID, instanceID)
func (v *View) UserSessionByIDs(ctx context.Context, agentID, userID, instanceID string) (*model.UserSessionView, error) {
return view.UserSessionByIDs(ctx, v.client, agentID, userID, instanceID)
}
func (v *View) UserSessionsByAgentID(agentID, instanceID string) ([]*model.UserSessionView, error) {
return view.UserSessionsByAgentID(v.client, agentID, instanceID)
func (v *View) UserSessionsByAgentID(ctx context.Context, agentID, instanceID string) ([]*model.UserSessionView, error) {
return view.UserSessionsByAgentID(ctx, v.client, agentID, instanceID)
}
func (v *View) UserAgentIDBySessionID(ctx context.Context, sessionID, instanceID string) (string, error) {
return view.UserAgentIDBySessionID(ctx, v.client, sessionID, instanceID)
}
func (v *View) ActiveUserIDsBySessionID(ctx context.Context, sessionID, instanceID string) (userAgentID string, userIDs []string, err error) {
return view.ActiveUserIDsBySessionID(ctx, v.client, sessionID, instanceID)
}
func (v *View) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (_ *query.CurrentState, err error) {

View File

@@ -6,4 +6,6 @@ import (
type UserRepository interface {
UserSessionUserIDsByAgentID(ctx context.Context, agentID string) ([]string, error)
UserAgentIDBySessionID(ctx context.Context, sessionID string) (string, error)
ActiveUserIDsBySessionID(ctx context.Context, sessionID string) (userAgentID string, userIDs []string, err error)
}

View File

@@ -50,6 +50,7 @@ func (c *Commands) ApproveDeviceAuth(
authTime time.Time,
preferredLanguage *language.Tag,
userAgent *domain.UserAgent,
sessionID string,
) (*domain.ObjectDetails, error) {
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode)
if err != nil {
@@ -58,7 +59,7 @@ func (c *Commands) ApproveDeviceAuth(
if !model.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound")
}
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent))
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID))
if err != nil {
return nil, err
}
@@ -151,7 +152,7 @@ func (c *Commands) CreateOIDCSessionFromDeviceAuth(ctx context.Context, deviceCo
cmd.AddSession(ctx,
deviceAuthModel.UserID,
deviceAuthModel.UserOrgID,
"",
deviceAuthModel.SessionID,
deviceAuthModel.ClientID,
deviceAuthModel.Audience,
deviceAuthModel.Scopes,

View File

@@ -28,6 +28,7 @@ type DeviceAuthWriteModel struct {
PreferredLanguage *language.Tag
UserAgent *domain.UserAgent
NeedRefreshToken bool
SessionID string
}
func NewDeviceAuthWriteModel(deviceCode, resourceOwner string) *DeviceAuthWriteModel {
@@ -60,6 +61,7 @@ func (m *DeviceAuthWriteModel) Reduce() error {
m.AuthTime = e.AuthTime
m.PreferredLanguage = e.PreferredLanguage
m.UserAgent = e.UserAgent
m.SessionID = e.SessionID
case *deviceauth.CanceledEvent:
m.State = e.Reason.State()
case *deviceauth.DoneEvent:

View File

@@ -137,6 +137,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
authTime time.Time
preferredLanguage *language.Tag
userAgent *domain.UserAgent
sessionID string
}
tests := []struct {
name string
@@ -161,6 +162,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
},
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound"),
},
@@ -188,6 +190,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
),
),
),
@@ -201,6 +204,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
},
wantErr: pushErr,
},
@@ -228,6 +232,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
),
),
),
@@ -241,6 +246,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
},
wantDetails: &domain.ObjectDetails{
ResourceOwner: "instance1",
@@ -252,7 +258,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.userID, tt.args.userOrgID, tt.args.authMethods, tt.args.authTime, tt.args.preferredLanguage, tt.args.userAgent)
gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.userID, tt.args.userOrgID, tt.args.authMethods, tt.args.authTime, tt.args.preferredLanguage, tt.args.userAgent, tt.args.sessionID)
require.ErrorIs(t, err, tt.wantErr)
assertObjectDetails(t, tt.wantDetails, gotDetails)
})
@@ -607,13 +613,14 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
),
),
),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "", &language.Afrikaans, &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
@@ -657,7 +664,8 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
Reason: domain.TokenReasonAuthRequest,
Reason: domain.TokenReasonAuthRequest,
SessionID: "sessionID",
},
},
{
@@ -687,13 +695,14 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) {
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
"sessionID",
),
),
),
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "", &language.Afrikaans, &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
@@ -742,6 +751,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) {
},
Reason: domain.TokenReasonAuthRequest,
RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID
SessionID: "sessionID",
},
},
}

View File

@@ -25,6 +25,7 @@ type InstanceFeatures struct {
ImprovedPerformance []feature.ImprovedPerformanceType
WebKey *bool
DebugOIDCParentError *bool
OIDCSingleV1SessionTermination *bool
}
func (m *InstanceFeatures) isEmpty() bool {
@@ -37,7 +38,8 @@ func (m *InstanceFeatures) isEmpty() bool {
// nil check to allow unset improvements
m.ImprovedPerformance == nil &&
m.WebKey == nil &&
m.DebugOIDCParentError == nil
m.DebugOIDCParentError == nil &&
m.OIDCSingleV1SessionTermination == nil
}
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {

View File

@@ -69,6 +69,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceImprovedPerformanceEventType,
feature_v2.InstanceWebKeyEventType,
feature_v2.InstanceDebugOIDCParentErrorEventType,
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -108,6 +109,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
case feature.KeyDebugOIDCParentError:
v := value.(bool)
features.DebugOIDCParentError = &v
case feature.KeyOIDCSingleV1SessionTermination:
v := value.(bool)
features.OIDCSingleV1SessionTermination = &v
}
}
@@ -123,5 +127,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType)
return cmds
}

View File

@@ -208,6 +208,10 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceActionsEventType, true,
),
feature_v2.NewSetEvent[bool](
ctx, aggregate,
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, true,
),
),
),
args: args{ctx, &InstanceFeatures{
@@ -216,6 +220,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(true),
OIDCSingleV1SessionTermination: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
@@ -246,6 +251,10 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
ctx, aggregate,
feature_v2.InstanceLegacyIntrospectionEventType, true,
)),
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, false,
),
),
expectPush(
feature_v2.NewSetEvent[bool](
@@ -262,6 +271,7 @@ func TestCommands_SetInstanceFeatures(t *testing.T) {
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: gu.Ptr(true),
OIDCSingleV1SessionTermination: gu.Ptr(false),
}},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",

View File

@@ -136,6 +136,7 @@ func (c *Commands) CreateOIDCSession(ctx context.Context,
reason domain.TokenReason,
actor *domain.TokenActor,
needRefreshToken bool,
sessionID string,
) (session *OIDCSession, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -151,7 +152,7 @@ func (c *Commands) CreateOIDCSession(ctx context.Context,
cmd.UserImpersonated(ctx, userID, resourceOwner, clientID, actor)
}
cmd.AddSession(ctx, userID, resourceOwner, "", clientID, audience, scope, authMethods, authTime, nonce, preferredLanguage, userAgent)
cmd.AddSession(ctx, userID, resourceOwner, sessionID, clientID, audience, scope, authMethods, authTime, nonce, preferredLanguage, userAgent)
if err = cmd.AddAccessToken(ctx, scope, userID, resourceOwner, reason, actor); err != nil {
return nil, err
}

View File

@@ -479,6 +479,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
reason domain.TokenReason
actor *domain.TokenActor
needRefreshToken bool
sessionID string
}
tests := []struct {
name string
@@ -684,6 +685,89 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID
},
},
{
name: "with sessionID",
fields: fields{
eventstore: expectEventstore(
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
),
oidcsession.NewAccessTokenAddedEvent(context.Background(),
&oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest,
&domain.TokenActor{
UserID: "user2",
Issuer: "foo.com",
},
),
user.NewUserTokenV2AddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "at_accessTokenID"),
),
),
idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID", "accessTokenID"),
defaultAccessTokenLifetime: time.Hour,
defaultRefreshTokenLifetime: 7 * 24 * time.Hour,
defaultRefreshTokenIdleLifetime: 24 * time.Hour,
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
userID: "userID",
resourceOwner: "org1",
clientID: "clientID",
audience: []string{"audience"},
scope: []string{"openid", "offline_access"},
authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
authTime: testNow,
nonce: "nonce",
preferredLanguage: &language.Afrikaans,
userAgent: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
reason: domain.TokenReasonAuthRequest,
actor: &domain.TokenActor{
UserID: "user2",
Issuer: "foo.com",
},
needRefreshToken: false,
sessionID: "sessionID",
},
want: &OIDCSession{
TokenID: "V2_oidcSessionID-at_accessTokenID",
ClientID: "clientID",
UserID: "userID",
Audience: []string{"audience"},
Expiration: time.Time{}.Add(time.Hour),
Scope: []string{"openid", "offline_access"},
AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
AuthTime: testNow,
Nonce: "nonce",
PreferredLanguage: &language.Afrikaans,
UserAgent: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
Reason: domain.TokenReasonAuthRequest,
Actor: &domain.TokenActor{
UserID: "user2",
Issuer: "foo.com",
},
SessionID: "sessionID",
},
},
{
name: "impersonation not allowed",
fields: fields{
@@ -839,6 +923,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) {
tt.args.reason,
tt.args.actor,
tt.args.needRefreshToken,
tt.args.sessionID,
)
require.ErrorIs(t, err, tt.wantErr)
if got != nil {

View File

@@ -17,6 +17,7 @@ type SystemFeatures struct {
UserSchema *bool
Actions *bool
ImprovedPerformance []feature.ImprovedPerformanceType
OIDCSingleV1SessionTermination *bool
}
func (m *SystemFeatures) isEmpty() bool {
@@ -27,7 +28,8 @@ func (m *SystemFeatures) isEmpty() bool {
m.TokenExchange == nil &&
m.Actions == nil &&
// nil check to allow unset improvements
m.ImprovedPerformance == nil
m.ImprovedPerformance == nil &&
m.OIDCSingleV1SessionTermination == nil
}
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {

View File

@@ -60,6 +60,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemTokenExchangeEventType,
feature_v2.SystemActionsEventType,
feature_v2.SystemImprovedPerformanceEventType,
feature_v2.SystemOIDCSingleV1SessionTerminationEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -92,6 +93,9 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) {
features.Actions = &v
case feature.KeyImprovedPerformance:
features.ImprovedPerformance = value.([]feature.ImprovedPerformanceType)
case feature.KeyOIDCSingleV1SessionTermination:
v := value.(bool)
features.OIDCSingleV1SessionTermination = &v
}
}
@@ -105,6 +109,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.SystemActionsEventType)
cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.SystemOIDCSingleV1SessionTerminationEventType)
return cmds
}

View File

@@ -176,6 +176,10 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemActionsEventType, true,
),
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemOIDCSingleV1SessionTerminationEventType, true,
),
),
),
args: args{context.Background(), &SystemFeatures{
@@ -184,6 +188,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(true),
OIDCSingleV1SessionTermination: gu.Ptr(true),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",
@@ -232,6 +237,10 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
context.Background(), aggregate,
feature_v2.SystemActionsEventType, false,
),
feature_v2.NewSetEvent[bool](
context.Background(), aggregate,
feature_v2.SystemOIDCSingleV1SessionTerminationEventType, false,
),
),
),
args: args{context.Background(), &SystemFeatures{
@@ -240,6 +249,7 @@ func TestCommands_SetSystemFeatures(t *testing.T) {
LegacyIntrospection: gu.Ptr(true),
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(false),
OIDCSingleV1SessionTermination: gu.Ptr(false),
}},
want: &domain.ObjectDetails{
ResourceOwner: "SYSTEM",

View File

@@ -62,6 +62,8 @@ type AuthRequest struct {
SAMLRequestID string
// orgID the policies were last loaded with
policyOrgID string
// SessionID is set to the computed sessionID of the login session table
SessionID string
}
func (a *AuthRequest) SetPolicyOrgID(id string) {

View File

@@ -146,18 +146,17 @@ func NewUpsertStatement(event eventstore.Event, conflictCols []Column, values []
conflictTarget[i] = col.Name
}
config := execConfig{
args: args,
}
config := execConfig{}
if len(values) == 0 {
config.err = ErrNoValues
}
updateCols, updateVals := getUpdateCols(values, conflictTarget)
updateCols, updateVals, args := getUpdateCols(values, conflictTarget, params, args)
if len(updateCols) == 0 || len(updateVals) == 0 {
config.err = ErrNoValues
}
config.args = args
q := func(config execConfig) string {
var updateStmt string
@@ -194,18 +193,78 @@ func OnlySetValueOnInsert(table string, value interface{}) *onlySetValueOnInsert
}
}
func getUpdateCols(cols []Column, conflictTarget []string) (updateCols, updateVals []string) {
type onlySetValueInCase struct {
Table string
Value interface{}
Condition Condition
}
func (c *onlySetValueInCase) GetValue() interface{} {
return c.Value
}
// ColumnChangedCondition checks the current value and if it changed to a specific new value
func ColumnChangedCondition(table, column string, currentValue, newValue interface{}) Condition {
return func(param string) (string, []any) {
index, _ := strconv.Atoi(param)
return fmt.Sprintf("%[1]s.%[2]s = $%[3]d AND EXCLUDED.%[2]s = $%[4]d", table, column, index, index+1), []any{currentValue, newValue}
}
}
// ColumnIsNullCondition checks if the current value is null
func ColumnIsNullCondition(table, column string) Condition {
return func(param string) (string, []any) {
return fmt.Sprintf("%[1]s.%[2]s IS NULL", table, column), nil
}
}
// ConditionOr links multiple Conditions by OR
func ConditionOr(conditions ...Condition) Condition {
return func(param string) (_ string, args []any) {
if len(conditions) == 0 {
return "", nil
}
b := strings.Builder{}
s, arg := conditions[0](param)
b.WriteString(s)
args = append(args, arg...)
for i := 1; i < len(conditions); i++ {
b.WriteString(" OR ")
s, condArgs := conditions[i](param)
b.WriteString(s)
args = append(args, condArgs...)
}
return b.String(), args
}
}
// OnlySetValueInCase will only update to the desired value if the condition applies
func OnlySetValueInCase(table string, value interface{}, condition Condition) *onlySetValueInCase {
return &onlySetValueInCase{
Table: table,
Value: value,
Condition: condition,
}
}
func getUpdateCols(cols []Column, conflictTarget, params []string, args []interface{}) (updateCols, updateVals []string, updatedArgs []interface{}) {
updateCols = make([]string, len(cols))
updateVals = make([]string, len(cols))
updatedArgs = args
for i := len(cols) - 1; i >= 0; i-- {
col := cols[i]
table := "EXCLUDED"
if onlyOnInsert, ok := col.Value.(*onlySetValueOnInsert); ok {
table = onlyOnInsert.Table
}
updateCols[i] = col.Name
updateVals[i] = table + "." + col.Name
switch v := col.Value.(type) {
case *onlySetValueOnInsert:
updateVals[i] = v.Table + "." + col.Name
case *onlySetValueInCase:
s, condArgs := v.Condition(strconv.Itoa(len(params) + 1))
updatedArgs = append(updatedArgs, condArgs...)
updateVals[i] = fmt.Sprintf("CASE WHEN %[1]s THEN EXCLUDED.%[2]s ELSE %[3]s.%[2]s END", s, col.Name, v.Table)
default:
updateVals[i] = "EXCLUDED" + "." + col.Name
}
for _, conflict := range conflictTarget {
if conflict == col.Name {
copy(updateCols[i:], updateCols[i+1:])
@@ -221,7 +280,7 @@ func getUpdateCols(cols []Column, conflictTarget []string) (updateCols, updateVa
}
}
return updateCols, updateVals
return updateCols, updateVals, updatedArgs
}
func NewUpdateStatement(event eventstore.Event, values []Column, conditions []Condition, opts ...execOption) *Statement {

View File

@@ -451,6 +451,55 @@ func TestNewUpsertStatement(t *testing.T) {
},
},
},
{
name: "correct all *onlySetValueInCase",
args: args{
table: "my_table",
event: &testEvent{
aggregateType: "agg",
sequence: 1,
previousSequence: 0,
},
conflictCols: []Column{
NewCol("col1", nil),
},
values: []Column{
{
Name: "col1",
Value: "val1",
},
{
Name: "col2",
Value: &onlySetValueInCase{
Table: "some.table",
Value: "val2",
Condition: ConditionOr(
ColumnChangedCondition("some.table", "val3", 0, 1),
ColumnIsNullCondition("some.table", "val3"),
),
},
},
},
},
want: want{
table: "my_table",
aggregateType: "agg",
sequence: 1,
previousSequence: 1,
executer: &wantExecuter{
params: []params{
{
query: "INSERT INTO my_table (col1, col2) VALUES ($1, $2) ON CONFLICT (col1) DO UPDATE SET col2 = CASE WHEN some.table.val3 = $3 AND EXCLUDED.val3 = $4 OR some.table.val3 IS NULL THEN EXCLUDED.col2 ELSE some.table.col2 END",
args: []interface{}{"val1", "val2", 0, 1},
},
},
shouldExecute: true,
},
isErr: func(err error) bool {
return err == nil
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -16,6 +16,7 @@ const (
KeyImprovedPerformance
KeyWebKey
KeyDebugOIDCParentError
KeyOIDCSingleV1SessionTermination
)
//go:generate enumer -type Level -transform snake -trimprefix Level
@@ -41,6 +42,7 @@ type Features struct {
ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"`
WebKey bool `json:"web_key,omitempty"`
DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"`
OIDCSingleV1SessionTermination bool `json:"terminate_single_v1_session,omitempty"`
}
type ImprovedPerformanceType int32

View File

@@ -7,11 +7,11 @@ import (
"strings"
)
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_error"
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_errorterminate_single_v1_session"
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163}
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 190}
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_error"
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_errorterminate_single_v1_session"
func (i Key) String() string {
if i < 0 || i >= Key(len(_KeyIndex)-1) {
@@ -34,9 +34,10 @@ func _KeyNoOp() {
_ = x[KeyImprovedPerformance-(7)]
_ = x[KeyWebKey-(8)]
_ = x[KeyDebugOIDCParentError-(9)]
_ = x[KeyOIDCSingleV1SessionTermination-(10)]
}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination}
var _KeyNameToValueMap = map[string]Key{
_KeyName[0:11]: KeyUnspecified,
@@ -59,6 +60,8 @@ var _KeyNameToValueMap = map[string]Key{
_KeyLowerName[133:140]: KeyWebKey,
_KeyName[140:163]: KeyDebugOIDCParentError,
_KeyLowerName[140:163]: KeyDebugOIDCParentError,
_KeyName[163:190]: KeyOIDCSingleV1SessionTermination,
_KeyLowerName[163:190]: KeyOIDCSingleV1SessionTermination,
}
var _KeyNames = []string{
@@ -72,6 +75,7 @@ var _KeyNames = []string{
_KeyName[113:133],
_KeyName[133:140],
_KeyName[140:163],
_KeyName[163:190],
}
// KeyString retrieves an enum value from the enum constants string name.

View File

@@ -0,0 +1,61 @@
InitCode:
Title: Inisialisasi Pengguna
PreHeader: Inisialisasi Pengguna
Subject: Inisialisasi Pengguna
Greeting: 'Halo {{.DisplayName}},'
Text: Pengguna ini telah dibuat. {{.PreferredLoginName}} untuk masuk. {{.Code}}) Jika Anda tidak meminta email ini, abaikan saja.
ButtonText: Selesaikan inisialisasi
PasswordReset:
Title: Setel ulang kata sandi
PreHeader: Setel ulang kata sandi
Subject: Setel ulang kata sandi
Greeting: 'Halo {{.DisplayName}},'
Text: Kami menerima permintaan pengaturan ulang kata sandi. {{.Code}}) Jika Anda tidak meminta email ini, abaikan saja.
ButtonText: Setel ulang kata sandi
VerifyEmail:
Title: Verifikasi email
PreHeader: Verifikasi email
Subject: Verifikasi email
Greeting: 'Halo {{.DisplayName}},'
Text: Email baru telah ditambahkan. {{.Code}}) Jika Anda tidak menambahkan email baru, harap abaikan email ini.
ButtonText: Verifikasi email
VerifyPhone:
Title: Verifikasi telepon
PreHeader: Verifikasi telepon
Subject: Verifikasi telepon
Greeting: 'Halo {{.DisplayName}},'
Text: 'Nomor telepon baru telah ditambahkan. {{.Code}}'
ButtonText: Verifikasi telepon
VerifyEmailOTP:
Title: Verifikasi Kata Sandi Sekali Pakai
PreHeader: Verifikasi Kata Sandi Sekali Pakai
Subject: Verifikasi Kata Sandi Sekali Pakai
Greeting: 'Halo {{.DisplayName}},'
Text: Silakan gunakan kata sandi satu kali {{.OTP}} untuk mengautentikasi dalam lima menit berikutnya atau klik tombol "Otentikasi".
ButtonText: Otentikasi
VerifySMSOTP:
Text: >-
{{.OTP}} adalah kata sandi satu kali Anda untuk {{ .Domain }}. {{.Expiry}}.
{{.Domain}} #{{.OTP}}
DomainClaimed:
Title: Domain telah diklaim
PreHeader: Ganti email/nama pengguna
Subject: Domain telah diklaim
Greeting: 'Halo {{.DisplayName}},'
Text: Domainnya {{.Domain}} telah diklaim oleh suatu organisasi. {{.Username}} bukan bagian dari organisasi ini. {{.TempUsername}}) untuk login ini.
ButtonText: Login
PasswordlessRegistration:
Title: Tambahkan Login Tanpa Kata Sandi
PreHeader: Tambahkan Login Tanpa Kata Sandi
Subject: Tambahkan Login Tanpa Kata Sandi
Greeting: 'Halo {{.DisplayName}},'
Text: Kami menerima permintaan untuk menambahkan token untuk login tanpa kata sandi.
ButtonText: Tambahkan Login Tanpa Kata Sandi
PasswordChange:
Title: Kata sandi pengguna telah berubah
PreHeader: Ubah kata sandi
Subject: Kata sandi pengguna telah berubah
Greeting: 'Halo {{.DisplayName}},'
Text: 'Kata sandi pengguna Anda telah berubah. '
ButtonText: Login

View File

@@ -18,6 +18,7 @@ type InstanceFeatures struct {
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
WebKey FeatureSource[bool]
DebugOIDCParentError FeatureSource[bool]
OIDCSingleV1SessionTermination FeatureSource[bool]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {

View File

@@ -69,6 +69,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceImprovedPerformanceEventType,
feature_v2.InstanceWebKeyEventType,
feature_v2.InstanceDebugOIDCParentErrorEventType,
feature_v2.InstanceOIDCSingleV1SessionTerminationEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -92,6 +93,7 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
m.instance.TokenExchange = m.system.TokenExchange
m.instance.Actions = m.system.Actions
m.instance.ImprovedPerformance = m.system.ImprovedPerformance
m.instance.OIDCSingleV1SessionTermination = m.system.OIDCSingleV1SessionTermination
return true
}
@@ -121,6 +123,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
features.WebKey.set(level, event.Value)
case feature.KeyDebugOIDCParentError:
features.DebugOIDCParentError.set(level, event.Value)
case feature.KeyOIDCSingleV1SessionTermination:
features.OIDCSingleV1SessionTermination.set(level, event.Value)
}
return nil
}

View File

@@ -96,6 +96,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceDebugOIDCParentErrorEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceOIDCSingleV1SessionTerminationEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),

View File

@@ -27,6 +27,7 @@ type SystemFeatures struct {
TokenExchange FeatureSource[bool]
Actions FeatureSource[bool]
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
OIDCSingleV1SessionTermination FeatureSource[bool]
}
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {

View File

@@ -57,6 +57,7 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemTokenExchangeEventType,
feature_v2.SystemActionsEventType,
feature_v2.SystemImprovedPerformanceEventType,
feature_v2.SystemOIDCSingleV1SessionTerminationEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -88,6 +89,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S
features.Actions.set(level, event.Value)
case feature.KeyImprovedPerformance:
features.ImprovedPerformance.set(level, event.Value)
case feature.KeyOIDCSingleV1SessionTermination:
features.OIDCSingleV1SessionTermination.set(level, event.Value)
}
return nil
}

View File

@@ -72,6 +72,7 @@ type ApprovedEvent struct {
AuthTime time.Time
PreferredLanguage *language.Tag
UserAgent *domain.UserAgent
SessionID string
}
func (e *ApprovedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
@@ -95,17 +96,19 @@ func NewApprovedEvent(
authTime time.Time,
preferredLanguage *language.Tag,
userAgent *domain.UserAgent,
sessionID string,
) *ApprovedEvent {
return &ApprovedEvent{
eventstore.NewBaseEventForPush(
BaseEvent: eventstore.NewBaseEventForPush(
ctx, aggregate, ApprovedEventType,
),
userID,
userOrgID,
userAuthMethods,
authTime,
preferredLanguage,
userAgent,
UserID: userID,
UserOrgID: userOrgID,
UserAuthMethods: userAuthMethods,
AuthTime: authTime,
PreferredLanguage: preferredLanguage,
UserAgent: userAgent,
SessionID: sessionID,
}
}

View File

@@ -14,6 +14,7 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
@@ -25,4 +26,5 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]])
}

View File

@@ -19,6 +19,7 @@ var (
SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange)
SystemActionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyActions)
SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance)
SystemOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyOIDCSingleV1SessionTermination)
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
@@ -30,6 +31,7 @@ var (
InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance)
InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey)
InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError)
InstanceOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyOIDCSingleV1SessionTermination)
)
const (

1365
internal/static/i18n/id.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ type UserSessionView struct {
MultiFactorVerification time.Time
MultiFactorVerificationType domain.MFAType
Sequence uint64
ID string
}
type UserSessionSearchRequest struct {

View File

@@ -0,0 +1,11 @@
SELECT
s.user_agent_id,
s.user_id
FROM auth.user_sessions s
JOIN auth.user_sessions s2
ON s.instance_id = s2.instance_id
AND s.user_agent_id = s2.user_agent_id
WHERE
s2.id = $1
AND s.instance_id = $2
AND s.state = 0;

View File

@@ -32,6 +32,7 @@ const (
UserSessionKeyPasswordlessVerification = "passwordless_verification"
UserSessionKeyExternalLoginVerification = "external_login_verification"
UserSessionKeySelectedIDPConfigID = "selected_idp_config_id"
UserSessionKeyID = "id"
)
type UserSessionView struct {
@@ -59,6 +60,12 @@ type UserSessionView struct {
MultiFactorVerificationType sql.NullInt32 `json:"-" gorm:"column:multi_factor_verification_type"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
ID sql.NullString `json:"id" gorm:"-"`
}
type ActiveUserAgentUserIDs struct {
UserAgentID string
UserIDs []string
}
type userAgentIDPayload struct {
@@ -95,6 +102,7 @@ func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
MultiFactorVerification: userSession.MultiFactorVerification.Time,
MultiFactorVerificationType: domain.MFAType(userSession.MultiFactorVerificationType.Int32),
Sequence: userSession.Sequence,
ID: userSession.ID.String,
}
}

View File

@@ -0,0 +1,7 @@
SELECT
s.user_agent_id
FROM auth.user_sessions s
WHERE
s.id = $1
AND s.instance_id = $2
LIMIT 1;

View File

@@ -17,7 +17,8 @@ SELECT s.creation_date,
s.multi_factor_verification,
s.multi_factor_verification_type,
s.sequence,
s.instance_id
s.instance_id,
s.id
FROM auth.user_sessions s
LEFT JOIN projections.users13 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users13_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id

View File

@@ -1,6 +1,7 @@
package view
import (
"context"
"database/sql"
_ "embed"
"errors"
@@ -16,8 +17,15 @@ var userSessionByIDQuery string
//go:embed user_sessions_by_user_agent.sql
var userSessionsByUserAgentQuery string
func UserSessionByIDs(db *database.DB, agentID, userID, instanceID string) (userSession *model.UserSessionView, err error) {
err = db.QueryRow(
//go:embed user_agent_by_user_session_id.sql
var userAgentByUserSessionIDQuery string
//go:embed active_user_ids_by_session_id.sql
var activeUserIDsBySessionIDQuery string
func UserSessionByIDs(ctx context.Context, db *database.DB, agentID, userID, instanceID string) (userSession *model.UserSessionView, err error) {
err = db.QueryRowContext(
ctx,
func(row *sql.Row) error {
userSession, err = scanUserSession(row)
return err
@@ -29,8 +37,10 @@ func UserSessionByIDs(db *database.DB, agentID, userID, instanceID string) (user
)
return userSession, err
}
func UserSessionsByAgentID(db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) {
err = db.Query(
func UserSessionsByAgentID(ctx context.Context, db *database.DB, agentID, instanceID string) (userSessions []*model.UserSessionView, err error) {
err = db.QueryContext(
ctx,
func(rows *sql.Rows) error {
userSessions, err = scanUserSessions(rows)
return err
@@ -42,6 +52,51 @@ func UserSessionsByAgentID(db *database.DB, agentID, instanceID string) (userSes
return userSessions, err
}
func UserAgentIDBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, err error) {
err = db.QueryRowContext(
ctx,
func(row *sql.Row) error {
return row.Scan(&userAgentID)
},
userAgentByUserSessionIDQuery,
sessionID,
instanceID,
)
return userAgentID, err
}
// ActiveUserIDsBySessionID returns all userIDs with an active session on the same user agent (its id is also returned) based on a sessionID
func ActiveUserIDsBySessionID(ctx context.Context, db *database.DB, sessionID, instanceID string) (userAgentID string, userIDs []string, err error) {
err = db.QueryContext(
ctx,
func(rows *sql.Rows) error {
userAgentID, userIDs, err = scanActiveUserAgentUserIDs(rows)
return err
},
activeUserIDsBySessionIDQuery,
sessionID,
instanceID,
)
return userAgentID, userIDs, err
}
func scanActiveUserAgentUserIDs(rows *sql.Rows) (userAgentID string, userIDs []string, err error) {
for rows.Next() {
var userID string
err := rows.Scan(
&userAgentID,
&userID)
if err != nil {
return "", nil, err
}
userIDs = append(userIDs, userID)
}
if err := rows.Close(); err != nil {
return "", nil, zerrors.ThrowInternal(err, "VIEW-Sbrws", "Errors.Query.CloseRows")
}
return userAgentID, userIDs, nil
}
func scanUserSession(row *sql.Row) (*model.UserSessionView, error) {
session := new(model.UserSessionView)
err := row.Scan(
@@ -65,6 +120,7 @@ func scanUserSession(row *sql.Row) (*model.UserSessionView, error) {
&session.MultiFactorVerificationType,
&session.Sequence,
&session.InstanceID,
&session.ID,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(nil, "VIEW-NGBs1", "Errors.UserSession.NotFound")
@@ -97,6 +153,7 @@ func scanUserSessions(rows *sql.Rows) ([]*model.UserSessionView, error) {
&session.MultiFactorVerificationType,
&session.Sequence,
&session.InstanceID,
&session.ID,
)
if err != nil {
return nil, err

View File

@@ -17,7 +17,8 @@ SELECT s.creation_date,
s.multi_factor_verification,
s.multi_factor_verification_type,
s.sequence,
s.instance_id
s.instance_id,
s.id
FROM auth.user_sessions s
LEFT JOIN projections.users13 u ON s.user_id = u.id AND s.instance_id = u.instance_id
LEFT JOIN projections.users13_humans h ON s.user_id = h.user_id AND s.instance_id = h.instance_id