From 4d593dace2922d57ab09dfc8959a0200e90ddf76 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:50:53 +0200 Subject: [PATCH] fix: add domain as attribute to list user auth methods (#8718) # Which Problems Are Solved There is no option to only query auth methods related to specific domains. # How the Problems Are Solved Add domain as attribute to the ListAuthenticationMethodTypes request. # Additional Changes OwnerRemoved column removed from the projection. # Additional Context Closes #8615 --------- Co-authored-by: Livio Spring --- .../user/v2/integration_test/email_test.go | 10 +- .../user/v2/integration_test/idp_link_test.go | 6 + .../grpc/user/v2/integration_test/otp_test.go | 8 + .../user/v2/integration_test/passkey_test.go | 10 + .../user/v2/integration_test/password_test.go | 8 +- .../user/v2/integration_test/phone_test.go | 12 +- .../user/v2/integration_test/query_test.go | 6 + .../user/v2/integration_test/totp_test.go | 6 + .../grpc/user/v2/integration_test/u2f_test.go | 6 + .../user/v2/integration_test/user_test.go | 179 +++++++++++++++ internal/api/grpc/user/v2/user.go | 2 +- .../v2beta/integration_test/email_test.go | 6 + .../user/v2beta/integration_test/otp_test.go | 8 + .../v2beta/integration_test/passkey_test.go | 6 + .../v2beta/integration_test/password_test.go | 4 + .../v2beta/integration_test/phone_test.go | 8 + .../v2beta/integration_test/query_test.go | 6 + .../user/v2beta/integration_test/totp_test.go | 6 + .../user/v2beta/integration_test/u2f_test.go | 4 + .../user/v2beta/integration_test/user_test.go | 26 +++ internal/api/grpc/user/v2beta/user.go | 2 +- internal/api/ui/login/mail_verify_handler.go | 2 +- internal/query/projection/user_auth_method.go | 40 ++-- .../query/projection/user_auth_method_test.go | 81 +++++-- internal/query/user_auth_method.go | 42 ++-- internal/query/user_auth_method_test.go | 204 ++++++++++++++++-- internal/user/repository/view/user_by_id.sql | 2 +- internal/webauthn/client.go | 18 +- proto/zitadel/user/v2/user_service.proto | 17 +- 29 files changed, 649 insertions(+), 86 deletions(-) diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index c15e2c0bf..cbb82d263 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -13,11 +13,13 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func TestServer_SetEmail(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -144,6 +146,8 @@ func TestServer_SetEmail(t *testing.T) { } func TestServer_ResendEmailCode(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() @@ -245,6 +249,8 @@ func TestServer_ResendEmailCode(t *testing.T) { } func TestServer_VerifyEmail(t *testing.T) { + t.Parallel() + userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string diff --git a/internal/api/grpc/user/v2/integration_test/idp_link_test.go b/internal/api/grpc/user/v2/integration_test/idp_link_test.go index 2daa42b91..cee42c2ae 100644 --- a/internal/api/grpc/user/v2/integration_test/idp_link_test.go +++ b/internal/api/grpc/user/v2/integration_test/idp_link_test.go @@ -19,6 +19,8 @@ import ( ) func TestServer_AddIDPLink(t *testing.T) { + t.Parallel() + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -99,6 +101,8 @@ func TestServer_AddIDPLink(t *testing.T) { } func TestServer_ListIDPLinks(t *testing.T) { + t.Parallel() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) instanceIdpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) @@ -258,6 +262,8 @@ func TestServer_ListIDPLinks(t *testing.T) { } func TestServer_RemoveIDPLink(t *testing.T) { + t.Parallel() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) instanceIdpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) diff --git a/internal/api/grpc/user/v2/integration_test/otp_test.go b/internal/api/grpc/user/v2/integration_test/otp_test.go index ae7c04042..3070a1d58 100644 --- a/internal/api/grpc/user/v2/integration_test/otp_test.go +++ b/internal/api/grpc/user/v2/integration_test/otp_test.go @@ -15,6 +15,8 @@ import ( ) func TestServer_AddOTPSMS(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -121,6 +123,8 @@ func TestServer_AddOTPSMS(t *testing.T) { } func TestServer_RemoveOTPSMS(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -187,6 +191,8 @@ func TestServer_RemoveOTPSMS(t *testing.T) { } func TestServer_AddOTPEmail(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -295,6 +301,8 @@ func TestServer_AddOTPEmail(t *testing.T) { } func TestServer_RemoveOTPEmail(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2/integration_test/passkey_test.go b/internal/api/grpc/user/v2/integration_test/passkey_test.go index 6041e5e40..bba548dfc 100644 --- a/internal/api/grpc/user/v2/integration_test/passkey_test.go +++ b/internal/api/grpc/user/v2/integration_test/passkey_test.go @@ -19,6 +19,8 @@ import ( ) func TestServer_RegisterPasskey(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ UserId: userID, @@ -139,6 +141,8 @@ func TestServer_RegisterPasskey(t *testing.T) { } func TestServer_VerifyPasskeyRegistration(t *testing.T) { + t.Parallel() + userID, pkr := userWithPasskeyRegistered(t) attestationResponse, err := Instance.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) @@ -215,6 +219,8 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { } func TestServer_CreatePasskeyRegistrationLink(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() type args struct { @@ -348,6 +354,8 @@ func passkeyVerify(t *testing.T, userID string, pkr *user.RegisterPasskeyRespons } func TestServer_RemovePasskey(t *testing.T) { + t.Parallel() + userIDWithout := Instance.CreateHumanUser(CTX).GetUserId() userIDRegistered, pkrRegistered := userWithPasskeyRegistered(t) userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t) @@ -453,6 +461,8 @@ func TestServer_RemovePasskey(t *testing.T) { } func TestServer_ListPasskeys(t *testing.T) { + t.Parallel() + userIDWithout := Instance.CreateHumanUser(CTX).GetUserId() userIDRegistered, _ := userWithPasskeyRegistered(t) userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t) diff --git a/internal/api/grpc/user/v2/integration_test/password_test.go b/internal/api/grpc/user/v2/integration_test/password_test.go index a316b40df..00ac0bda8 100644 --- a/internal/api/grpc/user/v2/integration_test/password_test.go +++ b/internal/api/grpc/user/v2/integration_test/password_test.go @@ -12,11 +12,13 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func TestServer_RequestPasswordReset(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -104,6 +106,8 @@ func TestServer_RequestPasswordReset(t *testing.T) { } func TestServer_SetPassword(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2/integration_test/phone_test.go b/internal/api/grpc/user/v2/integration_test/phone_test.go index 456b77231..f757b0acb 100644 --- a/internal/api/grpc/user/v2/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2/integration_test/phone_test.go @@ -14,11 +14,13 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2" - user "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func TestServer_SetPhone(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -122,6 +124,8 @@ func TestServer_SetPhone(t *testing.T) { } func TestServer_ResendPhoneCode(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() @@ -196,6 +200,8 @@ func TestServer_ResendPhoneCode(t *testing.T) { } func TestServer_VerifyPhone(t *testing.T) { + t.Parallel() + userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string @@ -248,6 +254,8 @@ func TestServer_VerifyPhone(t *testing.T) { } func TestServer_RemovePhone(t *testing.T) { + t.Parallel() + userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2/integration_test/query_test.go b/internal/api/grpc/user/v2/integration_test/query_test.go index 5d0835d9a..b4d332e6c 100644 --- a/internal/api/grpc/user/v2/integration_test/query_test.go +++ b/internal/api/grpc/user/v2/integration_test/query_test.go @@ -19,6 +19,8 @@ import ( ) func TestServer_GetUserByID(t *testing.T) { + t.Parallel() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) type args struct { ctx context.Context @@ -187,6 +189,8 @@ func TestServer_GetUserByID(t *testing.T) { } func TestServer_GetUserByID_Permission(t *testing.T) { + t.Parallel() + timeNow := time.Now().UTC() newOrgOwnerEmail := fmt.Sprintf("%d@permission.get.com", timeNow.UnixNano()) newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) @@ -329,6 +333,8 @@ type userAttr struct { } func TestServer_ListUsers(t *testing.T) { + t.Parallel() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) userResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listusers.com", time.Now().UnixNano())) type args struct { diff --git a/internal/api/grpc/user/v2/integration_test/totp_test.go b/internal/api/grpc/user/v2/integration_test/totp_test.go index e65756c1c..add67876f 100644 --- a/internal/api/grpc/user/v2/integration_test/totp_test.go +++ b/internal/api/grpc/user/v2/integration_test/totp_test.go @@ -18,6 +18,8 @@ import ( ) func TestServer_RegisterTOTP(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -104,6 +106,8 @@ func TestServer_RegisterTOTP(t *testing.T) { } func TestServer_VerifyTOTPRegistration(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -207,6 +211,8 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { } func TestServer_RemoveTOTP(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2/integration_test/u2f_test.go b/internal/api/grpc/user/v2/integration_test/u2f_test.go index b8af753f8..9ec001124 100644 --- a/internal/api/grpc/user/v2/integration_test/u2f_test.go +++ b/internal/api/grpc/user/v2/integration_test/u2f_test.go @@ -17,6 +17,8 @@ import ( ) func TestServer_RegisterU2F(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() otherUser := Instance.CreateHumanUser(CTX).GetUserId() @@ -106,6 +108,8 @@ func TestServer_RegisterU2F(t *testing.T) { } func TestServer_VerifyU2FRegistration(t *testing.T) { + t.Parallel() + ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t) attestationResponse, err := Instance.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) @@ -211,6 +215,8 @@ func ctxFromNewUserWithVerifiedU2F(t *testing.T) (context.Context, string, strin } func TestServer_RemoveU2F(t *testing.T) { + t.Parallel() + userIDWithout := Instance.CreateHumanUser(CTX).GetUserId() ctxRegistered, userIDRegistered, pkrRegistered := ctxFromNewUserWithRegisteredU2F(t) _, userIDVerified, u2fVerified := ctxFromNewUserWithVerifiedU2F(t) diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index bbd6de832..8790030b5 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -18,9 +18,11 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/auth" "github.com/zitadel/zitadel/pkg/grpc/idp" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" + user_v1 "github.com/zitadel/zitadel/pkg/grpc/user" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) @@ -50,6 +52,8 @@ func TestMain(m *testing.M) { } func TestServer_AddHumanUser(t *testing.T) { + t.Parallel() + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -672,6 +676,8 @@ func TestServer_AddHumanUser(t *testing.T) { } func TestServer_AddHumanUser_Permission(t *testing.T) { + t.Parallel() + newOrgOwnerEmail := fmt.Sprintf("%d@permission.com", time.Now().UnixNano()) newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) type args struct { @@ -865,6 +871,8 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } func TestServer_UpdateHumanUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1221,6 +1229,8 @@ func TestServer_UpdateHumanUser(t *testing.T) { } func TestServer_UpdateHumanUser_Permission(t *testing.T) { + t.Parallel() + newOrgOwnerEmail := fmt.Sprintf("%d@permission.update.com", time.Now().UnixNano()) newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -1304,6 +1314,8 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } func TestServer_LockUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.LockUserRequest @@ -1412,6 +1424,8 @@ func TestServer_LockUser(t *testing.T) { } func TestServer_UnLockUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.UnlockUserRequest @@ -1520,6 +1534,8 @@ func TestServer_UnLockUser(t *testing.T) { } func TestServer_DeactivateUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.DeactivateUserRequest @@ -1628,6 +1644,8 @@ func TestServer_DeactivateUser(t *testing.T) { } func TestServer_ReactivateUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.ReactivateUserRequest @@ -1736,6 +1754,8 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { + t.Parallel() + projectResp, err := Instance.CreateProject(CTX) require.NoError(t, err) type args struct { @@ -1835,6 +1855,8 @@ func TestServer_DeleteUser(t *testing.T) { } func TestServer_StartIdentityProviderIntent(t *testing.T) { + t.Parallel() + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) orgIdpResp := Instance.AddOrgGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("NotDefaultOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) @@ -2099,6 +2121,8 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { /* func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { + t.Parallel() + idpID := Instance.AddGenericOAuthProvider(t, CTX) intentID := Instance.CreateIntent(t, CTX, idpID) successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", "id") @@ -2358,7 +2382,37 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { } */ +func ctxFromNewUserWithRegisteredPasswordlessLegacy(t *testing.T) (context.Context, string, *auth.AddMyPasswordlessResponse) { + userID := Instance.CreateHumanUser(CTX).GetUserId() + Instance.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) + ctx := integration.WithAuthorizationToken(CTX, sessionToken) + + pkr, err := Instance.Client.Auth.AddMyPasswordless(ctx, &auth.AddMyPasswordlessRequest{}) + require.NoError(t, err) + require.NotEmpty(t, pkr.GetKey()) + return ctx, userID, pkr +} + +func ctxFromNewUserWithVerifiedPasswordlessLegacy(t *testing.T) (context.Context, string) { + ctx, userID, pkr := ctxFromNewUserWithRegisteredPasswordlessLegacy(t) + + attestationResponse, err := Instance.WebAuthN.CreateAttestationResponseData(pkr.GetKey().GetPublicKey()) + require.NoError(t, err) + + _, err = Instance.Client.Auth.VerifyMyPasswordless(ctx, &auth.VerifyMyPasswordlessRequest{ + Verification: &user_v1.WebAuthNVerification{ + TokenName: "Mickey", + PublicKeyCredential: attestationResponse, + }, + }) + require.NoError(t, err) + return ctx, userID +} + func TestServer_ListAuthenticationMethodTypes(t *testing.T) { + t.Parallel() + userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId() userIDWithPasskey := Instance.CreateHumanUser(CTX).GetUserId() @@ -2395,6 +2449,9 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { }) require.NoError(t, err) + _, userLegacyID := ctxFromNewUserWithVerifiedPasswordlessLegacy(t) + require.NoError(t, err) + type args struct { ctx context.Context req *user.ListAuthenticationMethodTypesRequest @@ -2435,6 +2492,81 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { }, }, }, + { + name: "with auth (passkey) with domain", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userIDWithPasskey, + DomainQuery: &user.DomainQuery{ + Domain: Instance.Domain, + }, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + AuthMethodTypes: []user.AuthenticationMethodType{ + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY, + }, + }, + }, + { + name: "with auth (passkey) with wrong domain", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userIDWithPasskey, + DomainQuery: &user.DomainQuery{ + Domain: "notexistent", + }, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + }, + }, + }, + { + name: "with auth (passkey) with legacy", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userLegacyID, + DomainQuery: &user.DomainQuery{ + Domain: "notexistent", + }, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + }, + }, + }, + { + name: "with auth (passkey) with legacy included", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userLegacyID, + DomainQuery: &user.DomainQuery{ + Domain: "notexistent", + IncludeWithoutDomain: true, + }, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + AuthMethodTypes: []user.AuthenticationMethodType{ + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY, + }, + }, + }, { name: "multiple auth", args: args{ @@ -2453,6 +2585,47 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { }, }, }, + { + name: "multiple auth with domain", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userMultipleAuth, + DomainQuery: &user.DomainQuery{ + Domain: Instance.Domain, + }, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 2, + }, + AuthMethodTypes: []user.AuthenticationMethodType{ + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY, + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP, + }, + }, + }, + { + name: "multiple auth with wrong domain", + args: args{ + CTX, + &user.ListAuthenticationMethodTypesRequest{ + UserId: userMultipleAuth, + DomainQuery: &user.DomainQuery{ + Domain: "notexistent", + }, + }, + }, + want: &user.ListAuthenticationMethodTypesResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + AuthMethodTypes: []user.AuthenticationMethodType{ + user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2480,6 +2653,8 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { } func TestServer_CreateInviteCode(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.CreateInviteCodeRequest @@ -2610,6 +2785,8 @@ func TestServer_CreateInviteCode(t *testing.T) { } func TestServer_ResendInviteCode(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.ResendInviteCodeRequest @@ -2698,6 +2875,8 @@ func TestServer_ResendInviteCode(t *testing.T) { } func TestServer_VerifyInviteCode(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.VerifyInviteCodeRequest diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 13072e7c7..c0416f84a 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -587,7 +587,7 @@ func (s *Server) checkIntentToken(token string, intentID string) error { } func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { - authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true) + authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, req.GetDomainQuery().GetIncludeWithoutDomain(), req.GetDomainQuery().GetDomain()) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2beta/integration_test/email_test.go b/internal/api/grpc/user/v2beta/integration_test/email_test.go index 71d411fdb..073a43207 100644 --- a/internal/api/grpc/user/v2beta/integration_test/email_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/email_test.go @@ -18,6 +18,8 @@ import ( ) func TestServer_SetEmail(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -144,6 +146,8 @@ func TestServer_SetEmail(t *testing.T) { } func TestServer_ResendEmailCode(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() @@ -245,6 +249,8 @@ func TestServer_ResendEmailCode(t *testing.T) { } func TestServer_VerifyEmail(t *testing.T) { + t.Parallel() + userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string diff --git a/internal/api/grpc/user/v2beta/integration_test/otp_test.go b/internal/api/grpc/user/v2beta/integration_test/otp_test.go index 6d6e2eff3..3ffec36ef 100644 --- a/internal/api/grpc/user/v2beta/integration_test/otp_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/otp_test.go @@ -15,6 +15,8 @@ import ( ) func TestServer_AddOTPSMS(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -121,6 +123,8 @@ func TestServer_AddOTPSMS(t *testing.T) { } func TestServer_RemoveOTPSMS(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -187,6 +191,8 @@ func TestServer_RemoveOTPSMS(t *testing.T) { } func TestServer_AddOTPEmail(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -295,6 +301,8 @@ func TestServer_AddOTPEmail(t *testing.T) { } func TestServer_RemoveOTPEmail(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2beta/integration_test/passkey_test.go b/internal/api/grpc/user/v2beta/integration_test/passkey_test.go index acca01885..e5a0ec193 100644 --- a/internal/api/grpc/user/v2beta/integration_test/passkey_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/passkey_test.go @@ -18,6 +18,8 @@ import ( ) func TestServer_RegisterPasskey(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ UserId: userID, @@ -138,6 +140,8 @@ func TestServer_RegisterPasskey(t *testing.T) { } func TestServer_VerifyPasskeyRegistration(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ UserId: userID, @@ -226,6 +230,8 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { } func TestServer_CreatePasskeyRegistrationLink(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() type args struct { diff --git a/internal/api/grpc/user/v2beta/integration_test/password_test.go b/internal/api/grpc/user/v2beta/integration_test/password_test.go index 7dd71edbb..254a13a53 100644 --- a/internal/api/grpc/user/v2beta/integration_test/password_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/password_test.go @@ -17,6 +17,8 @@ import ( ) func TestServer_RequestPasswordReset(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -104,6 +106,8 @@ func TestServer_RequestPasswordReset(t *testing.T) { } func TestServer_SetPassword(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2beta/integration_test/phone_test.go b/internal/api/grpc/user/v2beta/integration_test/phone_test.go index baa7b0f89..afcbc35c4 100644 --- a/internal/api/grpc/user/v2beta/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/phone_test.go @@ -19,6 +19,8 @@ import ( ) func TestServer_SetPhone(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -122,6 +124,8 @@ func TestServer_SetPhone(t *testing.T) { } func TestServer_ResendPhoneCode(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId() @@ -196,6 +200,8 @@ func TestServer_ResendPhoneCode(t *testing.T) { } func TestServer_VerifyPhone(t *testing.T) { + t.Parallel() + userResp := Instance.CreateHumanUser(CTX) tests := []struct { name string @@ -248,6 +254,8 @@ func TestServer_VerifyPhone(t *testing.T) { } func TestServer_RemovePhone(t *testing.T) { + t.Parallel() + userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2beta/integration_test/query_test.go b/internal/api/grpc/user/v2beta/integration_test/query_test.go index f78a54d04..f7c12fd03 100644 --- a/internal/api/grpc/user/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/query_test.go @@ -28,6 +28,8 @@ func detailsV2ToV2beta(obj *object.Details) *object_v2beta.Details { } func TestServer_GetUserByID(t *testing.T) { + t.Parallel() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetUserByIDOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) type args struct { ctx context.Context @@ -196,6 +198,8 @@ func TestServer_GetUserByID(t *testing.T) { } func TestServer_GetUserByID_Permission(t *testing.T) { + t.Parallel() + timeNow := time.Now().UTC() newOrgOwnerEmail := fmt.Sprintf("%d@permission.get.com", timeNow.UnixNano()) newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("GetHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) @@ -338,6 +342,8 @@ type userAttr struct { } func TestServer_ListUsers(t *testing.T) { + t.Parallel() + orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) userResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listusers.com", time.Now().UnixNano())) type args struct { diff --git a/internal/api/grpc/user/v2beta/integration_test/totp_test.go b/internal/api/grpc/user/v2beta/integration_test/totp_test.go index 4afe5e1f3..9fcce4732 100644 --- a/internal/api/grpc/user/v2beta/integration_test/totp_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/totp_test.go @@ -18,6 +18,8 @@ import ( ) func TestServer_RegisterTOTP(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -104,6 +106,8 @@ func TestServer_RegisterTOTP(t *testing.T) { } func TestServer_VerifyTOTPRegistration(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) @@ -212,6 +216,8 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { } func TestServer_RemoveTOTP(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2beta/integration_test/u2f_test.go b/internal/api/grpc/user/v2beta/integration_test/u2f_test.go index 6e47cbbb9..c35231eef 100644 --- a/internal/api/grpc/user/v2beta/integration_test/u2f_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/u2f_test.go @@ -17,6 +17,8 @@ import ( ) func TestServer_RegisterU2F(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() otherUser := Instance.CreateHumanUser(CTX).GetUserId() @@ -106,6 +108,8 @@ func TestServer_RegisterU2F(t *testing.T) { } func TestServer_VerifyU2FRegistration(t *testing.T) { + t.Parallel() + userID := Instance.CreateHumanUser(CTX).GetUserId() Instance.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Instance.CreateVerifiedWebAuthNSession(t, CTX, userID) diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index bf93b67c9..587d6b5fe 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -50,6 +50,8 @@ func TestMain(m *testing.M) { } func TestServer_AddHumanUser(t *testing.T) { + t.Parallel() + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -631,6 +633,8 @@ func TestServer_AddHumanUser(t *testing.T) { } func TestServer_AddHumanUser_Permission(t *testing.T) { + t.Parallel() + newOrgOwnerEmail := fmt.Sprintf("%d@permission.com", time.Now().UnixNano()) newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) type args struct { @@ -824,6 +828,8 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } func TestServer_UpdateHumanUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1180,6 +1186,8 @@ func TestServer_UpdateHumanUser(t *testing.T) { } func TestServer_UpdateHumanUser_Permission(t *testing.T) { + t.Parallel() + newOrgOwnerEmail := fmt.Sprintf("%d@permission.update.com", time.Now().UnixNano()) newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman%d", time.Now().UnixNano()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -1263,6 +1271,8 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } func TestServer_LockUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.LockUserRequest @@ -1371,6 +1381,8 @@ func TestServer_LockUser(t *testing.T) { } func TestServer_UnLockUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.UnlockUserRequest @@ -1479,6 +1491,8 @@ func TestServer_UnLockUser(t *testing.T) { } func TestServer_DeactivateUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.DeactivateUserRequest @@ -1587,6 +1601,8 @@ func TestServer_DeactivateUser(t *testing.T) { } func TestServer_ReactivateUser(t *testing.T) { + t.Parallel() + type args struct { ctx context.Context req *user.ReactivateUserRequest @@ -1695,6 +1711,8 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { + t.Parallel() + projectResp, err := Instance.CreateProject(CTX) require.NoError(t, err) type args struct { @@ -1794,6 +1812,8 @@ func TestServer_DeleteUser(t *testing.T) { } func TestServer_AddIDPLink(t *testing.T) { + t.Parallel() + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -1874,6 +1894,8 @@ func TestServer_AddIDPLink(t *testing.T) { } func TestServer_StartIdentityProviderIntent(t *testing.T) { + t.Parallel() + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) orgIdpID := Instance.AddOrgGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("NotDefaultOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) @@ -2138,6 +2160,8 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { /* func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { + t.Parallel() + idpID := Instance.AddGenericOAuthProvider(t, CTX) intentID := Instance.CreateIntent(t, CTX, idpID) successfulID, token, changeDate, sequence := Instance.CreateSuccessfulOAuthIntent(t, CTX, idpID.Id, "", "id") @@ -2398,6 +2422,8 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { */ func TestServer_ListAuthenticationMethodTypes(t *testing.T) { + t.Parallel() + userIDWithoutAuth := Instance.CreateHumanUser(CTX).GetUserId() userIDWithPasskey := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index b6029a0d6..274c1d867 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -590,7 +590,7 @@ func (s *Server) checkIntentToken(token string, intentID string) error { } func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { - authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true) + authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, false, "") if err != nil { return nil, err } diff --git a/internal/api/ui/login/mail_verify_handler.go b/internal/api/ui/login/mail_verify_handler.go index a14aa197a..be1366352 100644 --- a/internal/api/ui/login/mail_verify_handler.go +++ b/internal/api/ui/login/mail_verify_handler.go @@ -72,7 +72,7 @@ func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) { } func (l *Login) checkUserNoFirstFactor(ctx context.Context, userID string) bool { - authMethods, err := l.query.ListUserAuthMethodTypes(setUserContext(ctx, userID, ""), userID, false) + authMethods, err := l.query.ListUserAuthMethodTypes(setUserContext(ctx, userID, ""), userID, false, false, "") if err != nil { logging.WithFields("userID", userID).OnError(err).Warn("unable to load user's auth methods for mail verification") return false diff --git a/internal/query/projection/user_auth_method.go b/internal/query/projection/user_auth_method.go index 1009579de..b986df155 100644 --- a/internal/query/projection/user_auth_method.go +++ b/internal/query/projection/user_auth_method.go @@ -14,7 +14,7 @@ import ( ) const ( - UserAuthMethodTable = "projections.user_auth_methods4" + UserAuthMethodTable = "projections.user_auth_methods5" UserAuthMethodUserIDCol = "user_id" UserAuthMethodTypeCol = "method_type" @@ -26,7 +26,7 @@ const ( UserAuthMethodInstanceIDCol = "instance_id" UserAuthMethodStateCol = "state" UserAuthMethodNameCol = "name" - UserAuthMethodOwnerRemovedCol = "owner_removed" + UserAuthMethodDomainCol = "domain" ) type userAuthMethodProjection struct{} @@ -52,11 +52,10 @@ func (*userAuthMethodProjection) Init() *old_handler.Check { handler.NewColumn(UserAuthMethodResourceOwnerCol, handler.ColumnTypeText), handler.NewColumn(UserAuthMethodInstanceIDCol, handler.ColumnTypeText), handler.NewColumn(UserAuthMethodNameCol, handler.ColumnTypeText), - handler.NewColumn(UserAuthMethodOwnerRemovedCol, handler.ColumnTypeBool, handler.Default(false)), + handler.NewColumn(UserAuthMethodDomainCol, handler.ColumnTypeText, handler.Nullable()), }, handler.NewPrimaryKey(UserAuthMethodInstanceIDCol, UserAuthMethodUserIDCol, UserAuthMethodTypeCol, UserAuthMethodTokenIDCol), handler.WithIndex(handler.NewIndex("resource_owner", []string{UserAuthMethodResourceOwnerCol})), - handler.WithIndex(handler.NewIndex("owner_removed", []string{UserAuthMethodOwnerRemovedCol})), ), ) } @@ -151,20 +150,37 @@ func (p *userAuthMethodProjection) Reducers() []handler.AggregateReducer { func (p *userAuthMethodProjection) reduceInitAuthMethod(event eventstore.Event) (*handler.Statement, error) { tokenID := "" + var rpID *string var methodType domain.UserAuthMethodType switch e := event.(type) { case *user.HumanPasswordlessAddedEvent: methodType = domain.UserAuthMethodTypePasswordless tokenID = e.WebAuthNTokenID + rpID = &e.RPID case *user.HumanU2FAddedEvent: methodType = domain.UserAuthMethodTypeU2F tokenID = e.WebAuthNTokenID + rpID = &e.RPID case *user.HumanOTPAddedEvent: methodType = domain.UserAuthMethodTypeTOTP default: return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType}) } - + cols := []handler.Column{ + handler.NewCol(UserAuthMethodTokenIDCol, tokenID), + handler.NewCol(UserAuthMethodCreationDateCol, handler.OnlySetValueOnInsert(UserAuthMethodTable, event.CreatedAt())), + handler.NewCol(UserAuthMethodChangeDateCol, event.CreatedAt()), + handler.NewCol(UserAuthMethodResourceOwnerCol, event.Aggregate().ResourceOwner), + handler.NewCol(UserAuthMethodInstanceIDCol, event.Aggregate().InstanceID), + handler.NewCol(UserAuthMethodUserIDCol, event.Aggregate().ID), + handler.NewCol(UserAuthMethodSequenceCol, event.Sequence()), + handler.NewCol(UserAuthMethodStateCol, domain.MFAStateNotReady), + handler.NewCol(UserAuthMethodTypeCol, methodType), + handler.NewCol(UserAuthMethodNameCol, ""), + } + if rpID != nil { + cols = append(cols, handler.NewCol(UserAuthMethodDomainCol, rpID)) + } return handler.NewUpsertStatement( event, []handler.Column{ @@ -173,18 +189,7 @@ func (p *userAuthMethodProjection) reduceInitAuthMethod(event eventstore.Event) handler.NewCol(UserAuthMethodTypeCol, nil), handler.NewCol(UserAuthMethodTokenIDCol, nil), }, - []handler.Column{ - handler.NewCol(UserAuthMethodTokenIDCol, tokenID), - handler.NewCol(UserAuthMethodCreationDateCol, handler.OnlySetValueOnInsert(UserAuthMethodTable, event.CreatedAt())), - handler.NewCol(UserAuthMethodChangeDateCol, event.CreatedAt()), - handler.NewCol(UserAuthMethodResourceOwnerCol, event.Aggregate().ResourceOwner), - handler.NewCol(UserAuthMethodInstanceIDCol, event.Aggregate().InstanceID), - handler.NewCol(UserAuthMethodUserIDCol, event.Aggregate().ID), - handler.NewCol(UserAuthMethodSequenceCol, event.Sequence()), - handler.NewCol(UserAuthMethodStateCol, domain.MFAStateNotReady), - handler.NewCol(UserAuthMethodTypeCol, methodType), - handler.NewCol(UserAuthMethodNameCol, ""), - }, + cols, ), nil } @@ -204,7 +209,6 @@ func (p *userAuthMethodProjection) reduceActivateEvent(event eventstore.Event) ( name = e.WebAuthNTokenName case *user.HumanOTPVerifiedEvent: methodType = domain.UserAuthMethodTypeTOTP - default: return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-f92f", "reduce.wrong.event.type %v", []eventstore.EventType{user.HumanPasswordlessTokenAddedType, user.HumanU2FTokenAddedType}) } diff --git a/internal/query/projection/user_auth_method_test.go b/internal/query/projection/user_auth_method_test.go index c7568bcab..fb3d6d9d9 100644 --- a/internal/query/projection/user_auth_method_test.go +++ b/internal/query/projection/user_auth_method_test.go @@ -3,6 +3,8 @@ package projection import ( "testing" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -30,7 +32,8 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { user.HumanPasswordlessTokenAddedType, user.AggregateType, []byte(`{ - "webAuthNTokenId": "token-id" + "webAuthNTokenId": "token-id", + "rpID": "example.com" }`), ), user.HumanPasswordlessAddedEventMapper), }, @@ -41,7 +44,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (instance_id, user_id, method_type, token_id) DO UPDATE SET (creation_date, change_date, resource_owner, sequence, state, name) = (projections.user_auth_methods4.creation_date, EXCLUDED.change_date, EXCLUDED.resource_owner, EXCLUDED.sequence, EXCLUDED.state, EXCLUDED.name)", + expectedStmt: "INSERT INTO projections.user_auth_methods5 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name, domain) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (instance_id, user_id, method_type, token_id) DO UPDATE SET (creation_date, change_date, resource_owner, sequence, state, name, domain) = (projections.user_auth_methods5.creation_date, EXCLUDED.change_date, EXCLUDED.resource_owner, EXCLUDED.sequence, EXCLUDED.state, EXCLUDED.name, EXCLUDED.domain)", expectedArgs: []interface{}{ "token-id", anyArg{}, @@ -53,6 +56,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { domain.MFAStateNotReady, domain.UserAuthMethodTypePasswordless, "", + gu.Ptr("example.com"), }, }, }, @@ -67,7 +71,8 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { user.HumanU2FTokenAddedType, user.AggregateType, []byte(`{ - "webAuthNTokenId": "token-id" + "webAuthNTokenId": "token-id", + "rpID": "example.com" }`), ), user.HumanU2FAddedEventMapper), }, @@ -78,7 +83,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (instance_id, user_id, method_type, token_id) DO UPDATE SET (creation_date, change_date, resource_owner, sequence, state, name) = (projections.user_auth_methods4.creation_date, EXCLUDED.change_date, EXCLUDED.resource_owner, EXCLUDED.sequence, EXCLUDED.state, EXCLUDED.name)", + expectedStmt: "INSERT INTO projections.user_auth_methods5 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name, domain) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (instance_id, user_id, method_type, token_id) DO UPDATE SET (creation_date, change_date, resource_owner, sequence, state, name, domain) = (projections.user_auth_methods5.creation_date, EXCLUDED.change_date, EXCLUDED.resource_owner, EXCLUDED.sequence, EXCLUDED.state, EXCLUDED.name, EXCLUDED.domain)", expectedArgs: []interface{}{ "token-id", anyArg{}, @@ -90,6 +95,46 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { domain.MFAStateNotReady, domain.UserAuthMethodTypeU2F, "", + gu.Ptr("example.com"), + }, + }, + }, + }, + }, + }, + { + name: "reduceAddedU2F internal", + args: args{ + event: getEvent( + testEvent( + user.HumanU2FTokenAddedType, + user.AggregateType, + []byte(`{ + "webAuthNTokenId": "token-id", + "rpID": "" + }`), + ), user.HumanU2FAddedEventMapper), + }, + reduce: (&userAuthMethodProjection{}).reduceInitAuthMethod, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.user_auth_methods5 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name, domain) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (instance_id, user_id, method_type, token_id) DO UPDATE SET (creation_date, change_date, resource_owner, sequence, state, name, domain) = (projections.user_auth_methods5.creation_date, EXCLUDED.change_date, EXCLUDED.resource_owner, EXCLUDED.sequence, EXCLUDED.state, EXCLUDED.name, EXCLUDED.domain)", + expectedArgs: []interface{}{ + "token-id", + anyArg{}, + anyArg{}, + "ro-id", + "instance-id", + "agg-id", + uint64(15), + domain.MFAStateNotReady, + domain.UserAuthMethodTypeU2F, + "", + gu.Ptr(""), }, }, }, @@ -114,7 +159,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (instance_id, user_id, method_type, token_id) DO UPDATE SET (creation_date, change_date, resource_owner, sequence, state, name) = (projections.user_auth_methods4.creation_date, EXCLUDED.change_date, EXCLUDED.resource_owner, EXCLUDED.sequence, EXCLUDED.state, EXCLUDED.name)", + expectedStmt: "INSERT INTO projections.user_auth_methods5 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (instance_id, user_id, method_type, token_id) DO UPDATE SET (creation_date, change_date, resource_owner, sequence, state, name) = (projections.user_auth_methods5.creation_date, EXCLUDED.change_date, EXCLUDED.resource_owner, EXCLUDED.sequence, EXCLUDED.state, EXCLUDED.name)", expectedArgs: []interface{}{ "", anyArg{}, @@ -152,7 +197,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_auth_methods4 SET (change_date, sequence, name, state) = ($1, $2, $3, $4) WHERE (user_id = $5) AND (method_type = $6) AND (resource_owner = $7) AND (token_id = $8) AND (instance_id = $9)", + expectedStmt: "UPDATE projections.user_auth_methods5 SET (change_date, sequence, name, state) = ($1, $2, $3, $4) WHERE (user_id = $5) AND (method_type = $6) AND (resource_owner = $7) AND (token_id = $8) AND (instance_id = $9)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -189,7 +234,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_auth_methods4 SET (change_date, sequence, name, state) = ($1, $2, $3, $4) WHERE (user_id = $5) AND (method_type = $6) AND (resource_owner = $7) AND (token_id = $8) AND (instance_id = $9)", + expectedStmt: "UPDATE projections.user_auth_methods5 SET (change_date, sequence, name, state) = ($1, $2, $3, $4) WHERE (user_id = $5) AND (method_type = $6) AND (resource_owner = $7) AND (token_id = $8) AND (instance_id = $9)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -224,7 +269,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_auth_methods4 SET (change_date, sequence, name, state) = ($1, $2, $3, $4) WHERE (user_id = $5) AND (method_type = $6) AND (resource_owner = $7) AND (token_id = $8) AND (instance_id = $9)", + expectedStmt: "UPDATE projections.user_auth_methods5 SET (change_date, sequence, name, state) = ($1, $2, $3, $4) WHERE (user_id = $5) AND (method_type = $6) AND (resource_owner = $7) AND (token_id = $8) AND (instance_id = $9)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -257,7 +302,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.user_auth_methods5 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "", anyArg{}, @@ -291,7 +336,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_auth_methods4 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedStmt: "INSERT INTO projections.user_auth_methods5 (token_id, creation_date, change_date, resource_owner, instance_id, user_id, sequence, state, method_type, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedArgs: []interface{}{ "", anyArg{}, @@ -327,7 +372,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)", expectedArgs: []interface{}{ "agg-id", domain.UserAuthMethodTypePasswordless, @@ -358,7 +403,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4) AND (token_id = $5)", expectedArgs: []interface{}{ "agg-id", domain.UserAuthMethodTypeU2F, @@ -387,7 +432,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "agg-id", domain.UserAuthMethodTypeTOTP, @@ -415,7 +460,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "agg-id", domain.UserAuthMethodTypeOTPSMS, @@ -443,7 +488,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "agg-id", domain.UserAuthMethodTypeOTPSMS, @@ -471,7 +516,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (user_id = $1) AND (method_type = $2) AND (resource_owner = $3) AND (instance_id = $4)", expectedArgs: []interface{}{ "agg-id", domain.UserAuthMethodTypeOTPEmail, @@ -500,7 +545,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (instance_id = $1) AND (resource_owner = $2)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (instance_id = $1) AND (resource_owner = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", @@ -527,7 +572,7 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_auth_methods4 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index ce919a912..3ba794ee0 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -63,8 +63,8 @@ var ( name: projection.UserAuthMethodTypeCol, table: userAuthMethodTable, } - UserAuthMethodColumnOwnerRemoved = Column{ - name: projection.UserAuthMethodOwnerRemovedCol, + UserAuthMethodColumnDomain = Column{ + name: projection.UserAuthMethodDomainCol, table: userAuthMethodTable, } @@ -72,11 +72,8 @@ var ( authMethodTypeUserID = UserAuthMethodColumnUserID.setTable(authMethodTypeTable) authMethodTypeInstanceID = UserAuthMethodColumnInstanceID.setTable(authMethodTypeTable) authMethodTypeType = UserAuthMethodColumnMethodType.setTable(authMethodTypeTable) - authMethodTypeTypes = Column{ - name: "method_types", - table: authMethodTypeTable, - } - authMethodTypeState = UserAuthMethodColumnState.setTable(authMethodTypeTable) + authMethodTypeState = UserAuthMethodColumnState.setTable(authMethodTypeTable) + authMethodTypeDomain = UserAuthMethodColumnDomain.setTable(authMethodTypeTable) userIDPsCountTable = idpUserLinkTable.setAlias("user_idps_count") userIDPsCountUserID = IDPUserLinkUserIDCol.setTable(userIDPsCountTable) @@ -140,7 +137,7 @@ func (q *UserAuthMethodSearchQueries) hasUserID() bool { } func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, permissionCheck domain.PermissionCheck) (userAuthMethods *AuthMethods, err error) { - methods, err := q.searchUserAuthMethods(ctx, queries, false) + methods, err := q.searchUserAuthMethods(ctx, queries) if err != nil { return nil, err } @@ -157,16 +154,12 @@ func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMe return methods, nil } -func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, withOwnerRemoved bool) (userAuthMethods *AuthMethods, err error) { +func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries) (userAuthMethods *AuthMethods, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareUserAuthMethodsQuery(ctx, q.client) - eq := sq.Eq{UserAuthMethodColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} - if !withOwnerRemoved { - eq[UserAuthMethodColumnOwnerRemoved.identifier()] = false - } - stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + stmt, args, err := queries.toQuery(query).Where(sq.Eq{UserAuthMethodColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-j9NJd", "Errors.Query.InvalidRequest") } @@ -182,7 +175,7 @@ func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMe return userAuthMethods, err } -func (q *Queries) ListUserAuthMethodTypes(ctx context.Context, userID string, activeOnly bool) (userAuthMethodTypes *AuthMethodTypes, err error) { +func (q *Queries) ListUserAuthMethodTypes(ctx context.Context, userID string, activeOnly bool, includeWithoutDomain bool, queryDomain string) (userAuthMethodTypes *AuthMethodTypes, err error) { ctxData := authz.GetCtxData(ctx) if ctxData.UserID != userID { if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil { @@ -192,7 +185,7 @@ func (q *Queries) ListUserAuthMethodTypes(ctx context.Context, userID string, ac ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - query, scan := prepareUserAuthMethodTypesQuery(ctx, q.client, activeOnly) + query, scan := prepareUserAuthMethodTypesQuery(ctx, q.client, activeOnly, includeWithoutDomain, queryDomain) eq := sq.Eq{ UserIDCol.identifier(): userID, UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -389,8 +382,8 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se } } -func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, activeOnly bool) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery(activeOnly) +func prepareUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase, activeOnly bool, includeWithoutDomain bool, queryDomain string) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery(activeOnly, includeWithoutDomain, queryDomain) if err != nil { return sq.SelectBuilder{}, nil } @@ -504,7 +497,7 @@ func prepareAuthMethodsIDPsQuery() (string, error) { return idpsQuery, err } -func prepareAuthMethodQuery(activeOnly bool) (string, []interface{}, error) { +func prepareAuthMethodQuery(activeOnly bool, includeWithoutDomain bool, queryDomain string) (string, []interface{}, error) { q := sq.Select( "DISTINCT("+authMethodTypeType.identifier()+")", authMethodTypeUserID.identifier(), @@ -513,6 +506,17 @@ func prepareAuthMethodQuery(activeOnly bool) (string, []interface{}, error) { if activeOnly { q = q.Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}) } + if queryDomain != "" { + conditions := sq.Or{ + sq.Eq{authMethodTypeDomain.identifier(): nil}, + sq.Eq{authMethodTypeDomain.identifier(): queryDomain}, + } + if includeWithoutDomain { + conditions = append(conditions, sq.Eq{authMethodTypeDomain.identifier(): ""}) + } + q = q.Where(conditions) + } + return q.ToSql() } diff --git a/internal/query/user_auth_method_test.go b/internal/query/user_auth_method_test.go index 7221129b0..9504a9db2 100644 --- a/internal/query/user_auth_method_test.go +++ b/internal/query/user_auth_method_test.go @@ -181,17 +181,17 @@ func TestUser_authMethodsCheckPermission(t *testing.T) { } var ( - prepareUserAuthMethodsStmt = `SELECT projections.user_auth_methods4.token_id,` + - ` projections.user_auth_methods4.creation_date,` + - ` projections.user_auth_methods4.change_date,` + - ` projections.user_auth_methods4.resource_owner,` + - ` projections.user_auth_methods4.user_id,` + - ` projections.user_auth_methods4.sequence,` + - ` projections.user_auth_methods4.name,` + - ` projections.user_auth_methods4.state,` + - ` projections.user_auth_methods4.method_type,` + + prepareUserAuthMethodsStmt = `SELECT projections.user_auth_methods5.token_id,` + + ` projections.user_auth_methods5.creation_date,` + + ` projections.user_auth_methods5.change_date,` + + ` projections.user_auth_methods5.resource_owner,` + + ` projections.user_auth_methods5.user_id,` + + ` projections.user_auth_methods5.sequence,` + + ` projections.user_auth_methods5.name,` + + ` projections.user_auth_methods5.state,` + + ` projections.user_auth_methods5.method_type,` + ` COUNT(*) OVER ()` + - ` FROM projections.user_auth_methods4` + + ` FROM projections.user_auth_methods5` + ` AS OF SYSTEM TIME '-1 ms'` prepareUserAuthMethodsCols = []string{ "token_id", @@ -210,7 +210,7 @@ var ( ` user_idps_count.count` + ` FROM projections.users13` + ` LEFT JOIN projections.users13_notifications ON projections.users13.id = projections.users13_notifications.user_id AND projections.users13.instance_id = projections.users13_notifications.instance_id` + - ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` + + ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods5 AS auth_method_types` + ` WHERE auth_method_types.state = $1) AS auth_method_types` + ` ON auth_method_types.user_id = projections.users13.id AND auth_method_types.instance_id = projections.users13.instance_id` + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + @@ -222,6 +222,40 @@ var ( "method_type", "idps_count", } + prepareActiveAuthMethodTypesDomainStmt = `SELECT projections.users13_notifications.password_set,` + + ` auth_method_types.method_type,` + + ` user_idps_count.count` + + ` FROM projections.users13` + + ` LEFT JOIN projections.users13_notifications ON projections.users13.id = projections.users13_notifications.user_id AND projections.users13.instance_id = projections.users13_notifications.instance_id` + + ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods5 AS auth_method_types` + + ` WHERE auth_method_types.state = $1 AND (auth_method_types.domain IS NULL OR auth_method_types.domain = $2 OR auth_method_types.domain = $3)) AS auth_method_types` + + ` ON auth_method_types.user_id = projections.users13.id AND auth_method_types.instance_id = projections.users13.instance_id` + + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + + ` ON user_idps_count.user_id = projections.users13.id AND user_idps_count.instance_id = projections.users13.instance_id` + + ` AS OF SYSTEM TIME '-1 ms` + prepareActiveAuthMethodTypesDomainCols = []string{ + "password_set", + "method_type", + "idps_count", + } + prepareActiveAuthMethodTypesDomainExternalStmt = `SELECT projections.users13_notifications.password_set,` + + ` auth_method_types.method_type,` + + ` user_idps_count.count` + + ` FROM projections.users13` + + ` LEFT JOIN projections.users13_notifications ON projections.users13.id = projections.users13_notifications.user_id AND projections.users13.instance_id = projections.users13_notifications.instance_id` + + ` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods5 AS auth_method_types` + + ` WHERE auth_method_types.state = $1 AND (auth_method_types.domain IS NULL OR auth_method_types.domain = $2)) AS auth_method_types` + + ` ON auth_method_types.user_id = projections.users13.id AND auth_method_types.instance_id = projections.users13.instance_id` + + ` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` + + ` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` + + ` ON user_idps_count.user_id = projections.users13.id AND user_idps_count.instance_id = projections.users13.instance_id` + + ` AS OF SYSTEM TIME '-1 ms` + prepareActiveAuthMethodTypesDomainExternalCols = []string{ + "password_set", + "method_type", + "idps_count", + } prepareAuthMethodTypesRequiredStmt = `SELECT projections.users13.type,` + ` auth_methods_force_mfa.force_mfa,` + ` auth_methods_force_mfa.force_mfa_local_only` + @@ -384,7 +418,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) { { name: "prepareUserAuthMethodTypesQuery no result", prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true) + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -401,7 +435,7 @@ func Test_UserAuthMethodPrepares(t *testing.T) { { name: "prepareUserAuthMethodTypesQuery one second factor", prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true) + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -430,10 +464,74 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, }, }, + { + name: "prepareUserAuthMethodTypesQuery one second factor with domain", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "example.com") + return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { + return scan(rows) + } + }, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareActiveAuthMethodTypesDomainStmt), + prepareActiveAuthMethodTypesDomainCols, + [][]driver.Value{ + { + true, + domain.UserAuthMethodTypePasswordless, + 1, + }, + }, + ), + }, + object: &AuthMethodTypes{ + SearchResponse: SearchResponse{ + Count: 3, + }, + AuthMethodTypes: []domain.UserAuthMethodType{ + domain.UserAuthMethodTypePasswordless, + domain.UserAuthMethodTypePassword, + domain.UserAuthMethodTypeIDP, + }, + }, + }, + { + name: "prepareUserAuthMethodTypesQuery one second factor with domain external", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, false, "example.com") + return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { + return scan(rows) + } + }, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareActiveAuthMethodTypesDomainExternalStmt), + prepareActiveAuthMethodTypesDomainExternalCols, + [][]driver.Value{ + { + true, + domain.UserAuthMethodTypePasswordless, + 1, + }, + }, + ), + }, + object: &AuthMethodTypes{ + SearchResponse: SearchResponse{ + Count: 3, + }, + AuthMethodTypes: []domain.UserAuthMethodType{ + domain.UserAuthMethodTypePasswordless, + domain.UserAuthMethodTypePassword, + domain.UserAuthMethodTypeIDP, + }, + }, + }, { name: "prepareUserAuthMethodTypesQuery multiple second factors", prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true) + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } @@ -468,10 +566,86 @@ func Test_UserAuthMethodPrepares(t *testing.T) { }, }, }, + { + name: "prepareUserAuthMethodTypesQuery multiple second factors domain", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "example.com") + return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { + return scan(rows) + } + }, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareActiveAuthMethodTypesDomainStmt), + prepareActiveAuthMethodTypesDomainCols, + [][]driver.Value{ + { + true, + domain.UserAuthMethodTypePasswordless, + 1, + }, + { + true, + domain.UserAuthMethodTypeTOTP, + 1, + }, + }, + ), + }, + object: &AuthMethodTypes{ + SearchResponse: SearchResponse{ + Count: 4, + }, + AuthMethodTypes: []domain.UserAuthMethodType{ + domain.UserAuthMethodTypePasswordless, + domain.UserAuthMethodTypeTOTP, + domain.UserAuthMethodTypePassword, + domain.UserAuthMethodTypeIDP, + }, + }, + }, + { + name: "prepareUserAuthMethodTypesQuery multiple second factors domain external", + prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, false, "example.com") + return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { + return scan(rows) + } + }, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareActiveAuthMethodTypesDomainExternalStmt), + prepareActiveAuthMethodTypesDomainExternalCols, + [][]driver.Value{ + { + true, + domain.UserAuthMethodTypePasswordless, + 1, + }, + { + true, + domain.UserAuthMethodTypeTOTP, + 1, + }, + }, + ), + }, + object: &AuthMethodTypes{ + SearchResponse: SearchResponse{ + Count: 4, + }, + AuthMethodTypes: []domain.UserAuthMethodType{ + domain.UserAuthMethodTypePasswordless, + domain.UserAuthMethodTypeTOTP, + domain.UserAuthMethodTypePassword, + domain.UserAuthMethodTypeIDP, + }, + }, + }, { name: "prepareUserAuthMethodTypesQuery sql err", prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) { - builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true) + builder, scan := prepareUserAuthMethodTypesQuery(ctx, db, true, true, "") return builder, func(rows *sql.Rows) (*AuthMethodTypes, error) { return scan(rows) } diff --git a/internal/user/repository/view/user_by_id.sql b/internal/user/repository/view/user_by_id.sql index 6362c6103..1720ad799 100644 --- a/internal/user/repository/view/user_by_id.sql +++ b/internal/user/repository/view/user_by_id.sql @@ -7,7 +7,7 @@ WITH auth_methods AS ( , instance_id , name FROM - projections.user_auth_methods4 + projections.user_auth_methods5 WHERE instance_id = $1 AND user_id = $2 diff --git a/internal/webauthn/client.go b/internal/webauthn/client.go index 62232154b..ff00a89c3 100644 --- a/internal/webauthn/client.go +++ b/internal/webauthn/client.go @@ -36,20 +36,28 @@ func (c *Client) CreateAttestationResponse(optionsPb *structpb.Struct) (*structp if err != nil { return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) } - parsedAttestationOptions, err := virtualwebauthn.ParseAttestationOptions(string(options)) + attestationResponse, err := c.CreateAttestationResponseData(options) if err != nil { - return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) + return nil, err } resp := new(structpb.Struct) - err = protojson.Unmarshal([]byte(virtualwebauthn.CreateAttestationResponse( - c.rp, c.auth, c.credential, *parsedAttestationOptions, - )), resp) + err = protojson.Unmarshal(attestationResponse, resp) if err != nil { return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) } return resp, nil } +func (c *Client) CreateAttestationResponseData(options []byte) ([]byte, error) { + parsedAttestationOptions, err := virtualwebauthn.ParseAttestationOptions(string(options)) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) + } + return []byte(virtualwebauthn.CreateAttestationResponse( + c.rp, c.auth, c.credential, *parsedAttestationOptions, + )), nil +} + func (c *Client) CreateAssertionResponse(optionsPb *structpb.Struct, verifyUser bool) (*structpb.Struct, error) { options, err := protojson.Marshal(optionsPb) if err != nil { diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 20d3b65b6..ac018f909 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -2133,6 +2133,21 @@ message ListAuthenticationMethodTypesRequest{ example: "\"69629026806489455\""; } ]; + optional DomainQuery domain_query = 2; +} + +message DomainQuery { + // List also auth method types without domain information like passkey and U2F added through V1 APIs / Login UI. + bool include_without_domain = 1; + // List only auth methods with specific domain. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"example.com\""; + } + ]; } message ListAuthenticationMethodTypesResponse{ @@ -2213,4 +2228,4 @@ message VerifyInviteCodeRequest { message VerifyInviteCodeResponse { zitadel.object.v2.Details details = 1; -} \ No newline at end of file +}