mirror of
https://github.com/zitadel/zitadel.git
synced 2025-03-01 00:17:24 +00:00
feat(api): list authentication method types in user api v2 (#6058)
This commit is contained in:
parent
82e7333169
commit
7046194530
@ -47,6 +47,7 @@ import (
|
|||||||
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
|
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
|
||||||
"github.com/zitadel/zitadel/internal/authz"
|
"github.com/zitadel/zitadel/internal/authz"
|
||||||
authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
|
authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
|
||||||
|
authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
|
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
|
||||||
@ -145,6 +146,11 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
|||||||
keys.SAML,
|
keys.SAML,
|
||||||
config.InternalAuthZ.RolePermissionMappings,
|
config.InternalAuthZ.RolePermissionMappings,
|
||||||
sessionTokenVerifier,
|
sessionTokenVerifier,
|
||||||
|
func(q *query.Queries) domain.PermissionCheck {
|
||||||
|
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||||
|
return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot start queries: %w", err)
|
return fmt.Errorf("cannot start queries: %w", err)
|
||||||
|
@ -221,3 +221,41 @@ func (s *Server) checkIntentToken(token string, intentID string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {
|
||||||
|
authMethods, err := s.query.ListActiveUserAuthMethodTypes(ctx, req.GetUserId(), false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.ListAuthenticationMethodTypesResponse{
|
||||||
|
Details: object.ToListDetails(authMethods.SearchResponse),
|
||||||
|
AuthMethodTypes: authMethodTypesToPb(authMethods.AuthMethodTypes),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType {
|
||||||
|
methods := make([]user.AuthenticationMethodType, len(methodTypes))
|
||||||
|
for i, method := range methodTypes {
|
||||||
|
methods[i] = authMethodTypeToPb(method)
|
||||||
|
}
|
||||||
|
return methods
|
||||||
|
}
|
||||||
|
|
||||||
|
func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.AuthenticationMethodType {
|
||||||
|
switch methodType {
|
||||||
|
case domain.UserAuthMethodTypeOTP:
|
||||||
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP
|
||||||
|
case domain.UserAuthMethodTypeU2F:
|
||||||
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F
|
||||||
|
case domain.UserAuthMethodTypePasswordless:
|
||||||
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||||
|
case domain.UserAuthMethodTypePassword:
|
||||||
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD
|
||||||
|
case domain.UserAuthMethodTypeIDP:
|
||||||
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP
|
||||||
|
case domain.UserAuthMethodTypeUnspecified:
|
||||||
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
|
||||||
|
default:
|
||||||
|
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||||
|
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
)
|
)
|
||||||
@ -712,3 +713,109 @@ func TestServer_RetrieveIdentityProviderInformation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_ListAuthenticationMethodTypes(t *testing.T) {
|
||||||
|
userIDWithoutAuth := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
|
||||||
|
userIDWithPasskey := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
Tester.RegisterUserPasskey(CTX, userIDWithPasskey)
|
||||||
|
|
||||||
|
userMultipleAuth := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
Tester.RegisterUserPasskey(CTX, userMultipleAuth)
|
||||||
|
provider, err := Tester.Client.Mgmt.AddGenericOIDCProvider(CTX, &mgmt.AddGenericOIDCProviderRequest{
|
||||||
|
Name: "ListAuthenticationMethodTypes",
|
||||||
|
Issuer: "https://example.com",
|
||||||
|
ClientId: "client_id",
|
||||||
|
ClientSecret: "client_secret",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
idpLink, err := Tester.Client.UserV2.AddIDPLink(CTX, &user.AddIDPLinkRequest{UserId: userMultipleAuth, IdpLink: &user.IDPLink{
|
||||||
|
IdpId: provider.GetId(),
|
||||||
|
UserId: "external-id",
|
||||||
|
UserName: "displayName",
|
||||||
|
}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *user.ListAuthenticationMethodTypesRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *user.ListAuthenticationMethodTypesResponse
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no auth",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&user.ListAuthenticationMethodTypesRequest{
|
||||||
|
UserId: userIDWithoutAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.ListAuthenticationMethodTypesResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with auth (passkey)",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&user.ListAuthenticationMethodTypesRequest{
|
||||||
|
UserId: userIDWithPasskey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.ListAuthenticationMethodTypesResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 1,
|
||||||
|
},
|
||||||
|
AuthMethodTypes: []user.AuthenticationMethodType{
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple auth",
|
||||||
|
args: args{
|
||||||
|
CTX,
|
||||||
|
&user.ListAuthenticationMethodTypesRequest{
|
||||||
|
UserId: userMultipleAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.ListAuthenticationMethodTypesResponse{
|
||||||
|
Details: &object.ListDetails{
|
||||||
|
TotalResult: 2,
|
||||||
|
},
|
||||||
|
AuthMethodTypes: []user.AuthenticationMethodType{
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var got *user.ListAuthenticationMethodTypesResponse
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
got, err = Client.ListAuthenticationMethodTypes(tt.args.ctx, tt.args.req)
|
||||||
|
if err == nil && got.GetDetails().GetProcessedSequence() >= idpLink.GetDetails().GetSequence() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-CTX.Done():
|
||||||
|
t.Fatal(CTX.Err(), err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Log("retrying ListAuthenticationMethodTypes")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want.GetDetails().GetTotalResult(), got.GetDetails().GetTotalResult())
|
||||||
|
require.Equal(t, tt.want.GetAuthMethodTypes(), got.GetAuthMethodTypes())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -222,3 +222,75 @@ func Test_intentToIDPInformationPb(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_authMethodTypesToPb(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
methodTypes []domain.UserAuthMethodType
|
||||||
|
want []user.AuthenticationMethodType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty list",
|
||||||
|
nil,
|
||||||
|
[]user.AuthenticationMethodType{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"list",
|
||||||
|
[]domain.UserAuthMethodType{
|
||||||
|
domain.UserAuthMethodTypePasswordless,
|
||||||
|
},
|
||||||
|
[]user.AuthenticationMethodType{
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, authMethodTypesToPb(tt.methodTypes), "authMethodTypesToPb(%v)", tt.methodTypes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_authMethodTypeToPb(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
methodType domain.UserAuthMethodType
|
||||||
|
want user.AuthenticationMethodType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"uspecified",
|
||||||
|
domain.UserAuthMethodTypeUnspecified,
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"(t)otp",
|
||||||
|
domain.UserAuthMethodTypeOTP,
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_TOTP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"u2f",
|
||||||
|
domain.UserAuthMethodTypeU2F,
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_U2F,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"passkey",
|
||||||
|
domain.UserAuthMethodTypePasswordless,
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSKEY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"password",
|
||||||
|
domain.UserAuthMethodTypePassword,
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_PASSWORD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idp",
|
||||||
|
domain.UserAuthMethodTypeIDP,
|
||||||
|
user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_IDP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, authMethodTypeToPb(tt.methodType), "authMethodTypeToPb(%v)", tt.methodType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -28,7 +28,7 @@ type PermissionCheck func(ctx context.Context, permission, orgID, resourceID str
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
PermissionUserWrite = "user.write"
|
PermissionUserWrite = "user.write"
|
||||||
PermissionSessionRead = "session.read"
|
PermissionUserRead = "user.read"
|
||||||
PermissionSessionWrite = "session.write"
|
PermissionSessionWrite = "session.write"
|
||||||
PermissionSessionDelete = "session.delete"
|
PermissionSessionDelete = "session.delete"
|
||||||
)
|
)
|
||||||
|
@ -51,6 +51,8 @@ const (
|
|||||||
UserAuthMethodTypeOTP
|
UserAuthMethodTypeOTP
|
||||||
UserAuthMethodTypeU2F
|
UserAuthMethodTypeU2F
|
||||||
UserAuthMethodTypePasswordless
|
UserAuthMethodTypePasswordless
|
||||||
|
UserAuthMethodTypePassword
|
||||||
|
UserAuthMethodTypeIDP
|
||||||
userAuthMethodTypeCount
|
userAuthMethodTypeCount
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||||
|
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
|
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
@ -17,6 +18,7 @@ import (
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
CC *grpc.ClientConn
|
CC *grpc.ClientConn
|
||||||
Admin admin.AdminServiceClient
|
Admin admin.AdminServiceClient
|
||||||
|
Mgmt mgmt.ManagementServiceClient
|
||||||
UserV2 user.UserServiceClient
|
UserV2 user.UserServiceClient
|
||||||
SessionV2 session.SessionServiceClient
|
SessionV2 session.SessionServiceClient
|
||||||
}
|
}
|
||||||
@ -25,6 +27,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
|||||||
return Client{
|
return Client{
|
||||||
CC: cc,
|
CC: cc,
|
||||||
Admin: admin.NewAdminServiceClient(cc),
|
Admin: admin.NewAdminServiceClient(cc),
|
||||||
|
Mgmt: mgmt.NewManagementServiceClient(cc),
|
||||||
UserV2: user.NewUserServiceClient(cc),
|
UserV2: user.NewUserServiceClient(cc),
|
||||||
SessionV2: session.NewSessionServiceClient(cc),
|
SessionV2: session.NewSessionServiceClient(cc),
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ type Queries struct {
|
|||||||
|
|
||||||
idpConfigEncryption crypto.EncryptionAlgorithm
|
idpConfigEncryption crypto.EncryptionAlgorithm
|
||||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
|
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
|
||||||
DefaultLanguage language.Tag
|
DefaultLanguage language.Tag
|
||||||
LoginDir http.FileSystem
|
LoginDir http.FileSystem
|
||||||
@ -55,6 +56,7 @@ func StartQueries(
|
|||||||
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||||
zitadelRoles []authz.RoleMapping,
|
zitadelRoles []authz.RoleMapping,
|
||||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
||||||
|
permissionCheck func(q *Queries) domain.PermissionCheck,
|
||||||
) (repo *Queries, err error) {
|
) (repo *Queries, err error) {
|
||||||
statikLoginFS, err := fs.NewWithNamespace("login")
|
statikLoginFS, err := fs.NewWithNamespace("login")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -95,6 +97,8 @@ func StartQueries(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repo.checkPermission = permissionCheck(repo)
|
||||||
|
|
||||||
err = projection.Create(ctx, sqlClient, es, projections, keyEncryptionAlgorithm, certEncryptionAlgorithm)
|
err = projection.Create(ctx, sqlClient, es, projections, keyEncryptionAlgorithm, certEncryptionAlgorithm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/call"
|
"github.com/zitadel/zitadel/internal/api/call"
|
||||||
@ -64,12 +65,27 @@ var (
|
|||||||
name: projection.UserAuthMethodOwnerRemovedCol,
|
name: projection.UserAuthMethodOwnerRemovedCol,
|
||||||
table: userAuthMethodTable,
|
table: userAuthMethodTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authMethodTypeTable = userAuthMethodTable.setAlias("auth_method_types")
|
||||||
|
authMethodTypeUserID = UserAuthMethodColumnUserID.setTable(authMethodTypeTable)
|
||||||
|
authMethodTypeInstanceID = UserAuthMethodColumnInstanceID.setTable(authMethodTypeTable)
|
||||||
|
authMethodTypeTypes = UserAuthMethodColumnMethodType.setTable(authMethodTypeTable)
|
||||||
|
authMethodTypeState = UserAuthMethodColumnState.setTable(authMethodTypeTable)
|
||||||
|
|
||||||
|
userIDPsCountTable = idpUserLinkTable.setAlias("user_idps_count")
|
||||||
|
userIDPsCountUserID = IDPUserLinkUserIDCol.setTable(userIDPsCountTable)
|
||||||
|
userIDPsCountInstanceID = IDPUserLinkInstanceIDCol.setTable(userIDPsCountTable)
|
||||||
|
userIDPsCountCount = Column{
|
||||||
|
name: "count",
|
||||||
|
table: userIDPsCountTable,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthMethods struct {
|
type AuthMethods struct {
|
||||||
SearchResponse
|
SearchResponse
|
||||||
AuthMethods []*AuthMethod
|
AuthMethods []*AuthMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthMethod struct {
|
type AuthMethod struct {
|
||||||
UserID string
|
UserID string
|
||||||
CreationDate time.Time
|
CreationDate time.Time
|
||||||
@ -83,6 +99,11 @@ type AuthMethod struct {
|
|||||||
Type domain.UserAuthMethodType
|
Type domain.UserAuthMethodType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthMethodTypes struct {
|
||||||
|
SearchResponse
|
||||||
|
AuthMethodTypes []domain.UserAuthMethodType
|
||||||
|
}
|
||||||
|
|
||||||
type UserAuthMethodSearchQueries struct {
|
type UserAuthMethodSearchQueries struct {
|
||||||
SearchRequest
|
SearchRequest
|
||||||
Queries []SearchQuery
|
Queries []SearchQuery
|
||||||
@ -114,6 +135,41 @@ func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMe
|
|||||||
return userAuthMethods, err
|
return userAuthMethods, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID string, withOwnerRemoved bool) (userAuthMethodTypes *AuthMethodTypes, err error) {
|
||||||
|
ctxData := authz.GetCtxData(ctx)
|
||||||
|
if ctxData.UserID != userID {
|
||||||
|
if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
query, scan := prepareActiveUserAuthMethodTypesQuery(ctx, q.client)
|
||||||
|
eq := sq.Eq{
|
||||||
|
UserIDCol.identifier(): userID,
|
||||||
|
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||||
|
}
|
||||||
|
if !withOwnerRemoved {
|
||||||
|
eq[UserOwnerRemovedCol.identifier()] = false
|
||||||
|
}
|
||||||
|
stmt, args, err := query.Where(eq).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.ThrowInvalidArgument(err, "QUERY-Sfdrg", "Errors.Query.InvalidRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := q.client.QueryContext(ctx, stmt, args...)
|
||||||
|
if err != nil || rows.Err() != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "QUERY-SDgr3", "Errors.Internal")
|
||||||
|
}
|
||||||
|
userAuthMethodTypes, err = scan(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userAuthMethodTypes.LatestSequence, err = q.latestSequence(ctx, userTable, notifyTable, userAuthMethodTable, idpUserLinkTable)
|
||||||
|
return userAuthMethodTypes, err
|
||||||
|
}
|
||||||
|
|
||||||
func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) {
|
func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) {
|
||||||
return NewTextQuery(UserAuthMethodColumnUserID, value, TextEquals)
|
return NewTextQuery(UserAuthMethodColumnUserID, value, TextEquals)
|
||||||
}
|
}
|
||||||
@ -253,3 +309,80 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) {
|
||||||
|
authMethodsQuery, authMethodsArgs, err := sq.Select(
|
||||||
|
"DISTINCT("+authMethodTypeTypes.identifier()+")",
|
||||||
|
authMethodTypeUserID.identifier(),
|
||||||
|
authMethodTypeInstanceID.identifier()).
|
||||||
|
From(authMethodTypeTable.identifier()).
|
||||||
|
Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return sq.SelectBuilder{}, nil
|
||||||
|
}
|
||||||
|
idpsQuery, _, err := sq.Select(
|
||||||
|
userIDPsCountUserID.identifier(),
|
||||||
|
userIDPsCountInstanceID.identifier(),
|
||||||
|
"COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
|
||||||
|
From(userIDPsCountTable.identifier()).
|
||||||
|
GroupBy(
|
||||||
|
userIDPsCountUserID.identifier(),
|
||||||
|
userIDPsCountInstanceID.identifier(),
|
||||||
|
).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return sq.SelectBuilder{}, nil
|
||||||
|
}
|
||||||
|
return sq.Select(
|
||||||
|
NotifyPasswordSetCol.identifier(),
|
||||||
|
authMethodTypeTypes.identifier(),
|
||||||
|
userIDPsCountCount.identifier()).
|
||||||
|
From(userTable.identifier()).
|
||||||
|
LeftJoin(join(NotifyUserIDCol, UserIDCol)).
|
||||||
|
LeftJoin("("+authMethodsQuery+") AS "+authMethodTypeTable.alias+" ON "+
|
||||||
|
authMethodTypeUserID.identifier()+" = "+UserIDCol.identifier()+" AND "+
|
||||||
|
authMethodTypeInstanceID.identifier()+" = "+UserInstanceIDCol.identifier(),
|
||||||
|
authMethodsArgs...).
|
||||||
|
LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " +
|
||||||
|
userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " +
|
||||||
|
userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))).
|
||||||
|
PlaceholderFormat(sq.Dollar),
|
||||||
|
func(rows *sql.Rows) (*AuthMethodTypes, error) {
|
||||||
|
userAuthMethodTypes := make([]domain.UserAuthMethodType, 0)
|
||||||
|
var passwordSet sql.NullBool
|
||||||
|
var idp sql.NullInt64
|
||||||
|
for rows.Next() {
|
||||||
|
var authMethodType sql.NullInt16
|
||||||
|
err := rows.Scan(
|
||||||
|
&passwordSet,
|
||||||
|
&authMethodType,
|
||||||
|
&idp,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if authMethodType.Valid {
|
||||||
|
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if passwordSet.Valid && passwordSet.Bool {
|
||||||
|
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypePassword)
|
||||||
|
}
|
||||||
|
if idp.Valid && idp.Int64 > 0 {
|
||||||
|
logging.Error("IDP", idp.Int64)
|
||||||
|
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypeIDP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, errors.ThrowInternal(err, "QUERY-3n9fl", "Errors.Query.CloseRows")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthMethodTypes{
|
||||||
|
AuthMethodTypes: userAuthMethodTypes,
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: uint64(len(userAuthMethodTypes)),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -36,6 +36,23 @@ var (
|
|||||||
"method_type",
|
"method_type",
|
||||||
"count",
|
"count",
|
||||||
}
|
}
|
||||||
|
prepareActiveAuthMethodTypesStmt = `SELECT projections.users8_notifications.password_set,` +
|
||||||
|
` auth_method_types.method_type,` +
|
||||||
|
` user_idps_count.count` +
|
||||||
|
` FROM projections.users8` +
|
||||||
|
` LEFT JOIN projections.users8_notifications ON projections.users8.id = projections.users8_notifications.user_id AND projections.users8.instance_id = projections.users8_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` +
|
||||||
|
` WHERE auth_method_types.state = $1) AS auth_method_types` +
|
||||||
|
` ON auth_method_types.user_id = projections.users8.id AND auth_method_types.instance_id = projections.users8.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.users8.id AND user_idps_count.instance_id = projections.users8.instance_id` +
|
||||||
|
` AS OF SYSTEM TIME '-1 ms`
|
||||||
|
prepareActiveAuthMethodTypesCols = []string{
|
||||||
|
"password_set",
|
||||||
|
"method_type",
|
||||||
|
"idps_count",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_UserAuthMethodPrepares(t *testing.T) {
|
func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||||
@ -182,6 +199,95 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
|
|||||||
},
|
},
|
||||||
object: nil,
|
object: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "prepareActiveUserAuthMethodTypesQuery no result",
|
||||||
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &AuthMethodTypes{AuthMethodTypes: []domain.UserAuthMethodType{}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepareActiveUserAuthMethodTypesQuery one second factor",
|
||||||
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
||||||
|
prepareActiveAuthMethodTypesCols,
|
||||||
|
[][]driver.Value{
|
||||||
|
{
|
||||||
|
true,
|
||||||
|
domain.UserAuthMethodTypePasswordless,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &AuthMethodTypes{
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: 3,
|
||||||
|
},
|
||||||
|
AuthMethodTypes: []domain.UserAuthMethodType{
|
||||||
|
domain.UserAuthMethodTypePasswordless,
|
||||||
|
domain.UserAuthMethodTypePassword,
|
||||||
|
domain.UserAuthMethodTypeIDP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepareActiveUserAuthMethodTypesQuery multiple second factors",
|
||||||
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueries(
|
||||||
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
||||||
|
prepareActiveAuthMethodTypesCols,
|
||||||
|
[][]driver.Value{
|
||||||
|
{
|
||||||
|
true,
|
||||||
|
domain.UserAuthMethodTypePasswordless,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
true,
|
||||||
|
domain.UserAuthMethodTypeOTP,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
object: &AuthMethodTypes{
|
||||||
|
SearchResponse: SearchResponse{
|
||||||
|
Count: 4,
|
||||||
|
},
|
||||||
|
AuthMethodTypes: []domain.UserAuthMethodType{
|
||||||
|
domain.UserAuthMethodTypePasswordless,
|
||||||
|
domain.UserAuthMethodTypeOTP,
|
||||||
|
domain.UserAuthMethodTypePassword,
|
||||||
|
domain.UserAuthMethodTypeIDP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepareActiveUserAuthMethodTypesQuery sql err",
|
||||||
|
prepare: prepareActiveUserAuthMethodTypesQuery,
|
||||||
|
want: want{
|
||||||
|
sqlExpectations: mockQueryErr(
|
||||||
|
regexp.QuoteMeta(prepareActiveAuthMethodTypesStmt),
|
||||||
|
sql.ErrConnDone,
|
||||||
|
),
|
||||||
|
err: func(err error) (error, bool) {
|
||||||
|
if !errors.Is(err, sql.ErrConnDone) {
|
||||||
|
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||||
|
}
|
||||||
|
return nil, true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
object: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -440,6 +440,30 @@ service UserService {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List all possible authentication methods of a user
|
||||||
|
rpc ListAuthenticationMethodTypes (ListAuthenticationMethodTypesRequest) returns (ListAuthenticationMethodTypesResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/v2alpha/users/{user_id}/authentication_methods"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.protoc_gen_zitadel.v2.options) = {
|
||||||
|
auth_option: {
|
||||||
|
permission: "authenticated"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "List all possible authentication methods of a user";
|
||||||
|
description: "List all possible authentication methods of a user like password, passwordless, (T)OTP and more";
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddHumanUserRequest{
|
message AddHumanUserRequest{
|
||||||
@ -857,6 +881,7 @@ message AddIDPLinkResponse {
|
|||||||
zitadel.object.v2alpha.Details details = 1;
|
zitadel.object.v2alpha.Details details = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
message PasswordResetRequest{
|
message PasswordResetRequest{
|
||||||
string user_id = 1 [
|
string user_id = 1 [
|
||||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
@ -919,3 +944,29 @@ message SetPasswordRequest{
|
|||||||
message SetPasswordResponse{
|
message SetPasswordResponse{
|
||||||
zitadel.object.v2alpha.Details details = 1;
|
zitadel.object.v2alpha.Details details = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ListAuthenticationMethodTypesRequest{
|
||||||
|
string user_id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"69629026806489455\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListAuthenticationMethodTypesResponse{
|
||||||
|
zitadel.object.v2alpha.ListDetails details = 1;
|
||||||
|
repeated AuthenticationMethodType auth_method_types = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthenticationMethodType {
|
||||||
|
AUTHENTICATION_METHOD_TYPE_UNSPECIFIED = 0;
|
||||||
|
AUTHENTICATION_METHOD_TYPE_PASSWORD = 1;
|
||||||
|
AUTHENTICATION_METHOD_TYPE_PASSKEY = 2;
|
||||||
|
AUTHENTICATION_METHOD_TYPE_IDP = 3;
|
||||||
|
AUTHENTICATION_METHOD_TYPE_TOTP = 4;
|
||||||
|
AUTHENTICATION_METHOD_TYPE_U2F = 5;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user