feat: api v2beta to api v2 (#8283)

# Which Problems Are Solved

The v2beta services are stable but not GA.

# How the Problems Are Solved

The v2beta services are copied to v2. The corresponding v1 and v2beta
services are deprecated.

# Additional Context

Closes #7236

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Stefan Benz 2024-07-26 22:39:55 +02:00 committed by GitHub
parent bc16962aac
commit 7d2d85f57c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
142 changed files with 15170 additions and 386 deletions

View File

@ -38,6 +38,6 @@ func VerifyDatabase(databaseName string) func(*database.DB) error {
return func(db *database.DB) error {
logging.WithFields("database", databaseName).Info("verify database")
return exec(db, fmt.Sprintf(string(databaseStmt), databaseName), []string{dbAlreadyExistsCode})
return exec(db, fmt.Sprintf(databaseStmt, databaseName), []string{dbAlreadyExistsCode})
}
}

View File

@ -95,7 +95,8 @@ func createEncryptionKeys(ctx context.Context, db *database.DB) error {
return err
}
if _, err = tx.Exec(createEncryptionKeysStmt); err != nil {
tx.Rollback()
rollbackErr := tx.Rollback()
logging.OnError(rollbackErr).Error("rollback failed")
return err
}
@ -110,7 +111,7 @@ func createEvents(ctx context.Context, db *database.DB) (err error) {
defer func() {
if err != nil {
rollbackErr := tx.Rollback()
logging.OnError(rollbackErr).Debug("rollback failed")
logging.OnError(rollbackErr).Error("rollback failed")
return
}
err = tx.Commit()

View File

@ -2,7 +2,6 @@ package key
import (
"errors"
"io/ioutil"
"os"
"github.com/spf13/cobra"
@ -42,7 +41,7 @@ func MasterKey(cmd *cobra.Command) (string, error) {
if masterKeyFromEnv {
return os.Getenv(envMasterKey), nil
}
data, err := ioutil.ReadFile(masterKeyFile)
data, err := os.ReadFile(masterKeyFile)
if err != nil {
return "", err
}

View File

@ -37,15 +37,21 @@ import (
action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/action/v3alpha"
"github.com/zitadel/zitadel/internal/api/grpc/admin"
"github.com/zitadel/zitadel/internal/api/grpc/auth"
"github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/management"
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
"github.com/zitadel/zitadel/internal/api/grpc/org/v2"
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
"github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta"
session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
settings_v2beta "github.com/zitadel/zitadel/internal/api/grpc/settings/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/system"
user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha"
user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/idp"
@ -399,20 +405,34 @@ func startAPIs(
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil {
if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, feature.CreateServer(commands, queries)); err != nil {
if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, session_v2.CreateServer(commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, org_v2.CreateServer(commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
@ -491,6 +511,9 @@ func startAPIs(
apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix)
// After OIDC provider so that the callback endpoint can be used
if err := apis.RegisterService(ctx, oidc_v2beta.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
return nil, err
}

View File

@ -1,5 +1,5 @@
---
title: APIs V2 (General Available)
title: APIs V2 (Generally Available)
---
import DocCardList from '@theme/DocCardList';
@ -7,4 +7,7 @@ import DocCardList from '@theme/DocCardList';
APIs V2 organize access by resources (users, settings, etc.), unlike context-specific V1 APIs.
This simplifies finding the right API, especially for multi-organization resources.
Users created with the V2 API have no initial state anymore, so new users are immediately active.
Requesting ZITADEL to send a verification email on user creation is still possible.
<DocCardList />

View File

@ -6,7 +6,7 @@
"docusaurus": "docusaurus",
"start": "docusaurus start",
"start:api": "yarn run generate && docusaurus start",
"build": "yarn run generate && docusaurus build",
"build": "yarn run generate && NODE_OPTIONS=--max-old-space-size=6144 docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",

View File

@ -12,7 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
@ -69,7 +69,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
Method: "/zitadel.session.v2.NotExistingService/List",
},
},
},
@ -86,7 +86,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
Method: "/zitadel.session.v2.SessionService/ListSessions",
},
},
},
@ -125,7 +125,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
Service: "zitadel.session.v2.SessionService",
},
},
},
@ -200,7 +200,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
Service: "zitadel.session.v2.SessionService",
},
},
},
@ -213,7 +213,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
Method: "/zitadel.session.v2.SessionService/ListSessions",
},
},
},
@ -247,7 +247,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
Method: "/zitadel.session.v2.SessionService/ListSessions",
},
},
},
@ -269,7 +269,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
Service: "zitadel.session.v2.SessionService",
},
},
},
@ -347,7 +347,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/NotExisting",
Method: "/zitadel.session.v2.SessionService/NotExisting",
},
},
},
@ -367,7 +367,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/GetSession",
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
@ -408,7 +408,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.user.v2beta.UserService",
Service: "zitadel.user.v2.UserService",
},
},
},
@ -512,7 +512,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
Method: "/zitadel.session.v2.NotExistingService/List",
},
},
},
@ -529,7 +529,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
Method: "/zitadel.session.v2.SessionService/ListSessions",
},
},
},
@ -568,7 +568,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
Service: "zitadel.session.v2.SessionService",
},
},
},
@ -670,7 +670,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/NotExisting",
Method: "/zitadel.session.v2.SessionService/NotExisting",
},
},
},
@ -690,7 +690,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/GetSession",
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
@ -731,7 +731,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "zitadel.user.v2beta.UserService",
Service: "zitadel.user.v2.UserService",
},
},
},

View File

@ -16,7 +16,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func TestServer_GetTargetByID(t *testing.T) {
@ -532,7 +532,7 @@ func TestServer_ListExecutions(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/GetSession",
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
@ -555,7 +555,7 @@ func TestServer_ListExecutions(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/GetSession",
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
@ -720,7 +720,7 @@ func TestServer_ListExecutions(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/GetSession",
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
@ -729,7 +729,7 @@ func TestServer_ListExecutions(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/CreateSession",
Method: "/zitadel.session.v2.SessionService/CreateSession",
},
},
},
@ -738,7 +738,7 @@ func TestServer_ListExecutions(t *testing.T) {
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/SetSession",
Method: "/zitadel.session.v2.SessionService/SetSession",
},
},
},
@ -795,11 +795,11 @@ func TestServer_ListExecutions(t *testing.T) {
Query: &action.SearchQuery_InConditionsQuery{
InConditionsQuery: &action.InConditionsQuery{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},

View File

@ -14,7 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
var (

View File

@ -17,7 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func TestServer_CreateTarget(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/query"
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {

View File

@ -12,8 +12,8 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/query"
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func Test_systemFeaturesToCommand(t *testing.T) {

View File

@ -7,7 +7,7 @@ import (
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {

View File

@ -14,8 +14,8 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
var (

View File

@ -7,7 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
type Server struct {

View File

@ -0,0 +1,155 @@
package feature
import (
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/query"
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
)
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
return &command.SystemFeatures{
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
LegacyIntrospection: req.OidcLegacyIntrospection,
UserSchema: req.UserSchema,
Actions: req.Actions,
TokenExchange: req.OidcTokenExchange,
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
}
}
func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse {
return &feature_pb.GetSystemFeaturesResponse{
Details: object.DomainToDetailsPb(f.Details),
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
UserSchema: featureSourceToFlagPb(&f.UserSchema),
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
}
}
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures {
return &command.InstanceFeatures{
LoginDefaultOrg: req.LoginDefaultOrg,
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
LegacyIntrospection: req.OidcLegacyIntrospection,
UserSchema: req.UserSchema,
TokenExchange: req.OidcTokenExchange,
Actions: req.Actions,
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
}
}
func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse {
return &feature_pb.GetInstanceFeaturesResponse{
Details: object.DomainToDetailsPb(f.Details),
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
UserSchema: featureSourceToFlagPb(&f.UserSchema),
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
}
}
func featureSourceToImprovedPerformanceFlagPb(fs *query.FeatureSource[[]feature.ImprovedPerformanceType]) *feature_pb.ImprovedPerformanceFeatureFlag {
return &feature_pb.ImprovedPerformanceFeatureFlag{
ExecutionPaths: improvedPerformanceTypesToPb(fs.Value),
Source: featureLevelToSourcePb(fs.Level),
}
}
func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag {
return &feature_pb.FeatureFlag{
Enabled: fs.Value,
Source: featureLevelToSourcePb(fs.Level),
}
}
func featureLevelToSourcePb(level feature.Level) feature_pb.Source {
switch level {
case feature.LevelUnspecified:
return feature_pb.Source_SOURCE_UNSPECIFIED
case feature.LevelSystem:
return feature_pb.Source_SOURCE_SYSTEM
case feature.LevelInstance:
return feature_pb.Source_SOURCE_INSTANCE
case feature.LevelOrg:
return feature_pb.Source_SOURCE_ORGANIZATION
case feature.LevelProject:
return feature_pb.Source_SOURCE_PROJECT
case feature.LevelApp:
return feature_pb.Source_SOURCE_APP
case feature.LevelUser:
return feature_pb.Source_SOURCE_USER
default:
return feature_pb.Source(level)
}
}
func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []feature_pb.ImprovedPerformance {
res := make([]feature_pb.ImprovedPerformance, len(types))
for i, typ := range types {
res[i] = improvedPerformanceTypeToPb(typ)
}
return res
}
func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance {
switch typ {
case feature.ImprovedPerformanceTypeUnknown:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED
case feature.ImprovedPerformanceTypeOrgByID:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID
case feature.ImprovedPerformanceTypeProjectGrant:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT
case feature.ImprovedPerformanceTypeProject:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT
case feature.ImprovedPerformanceTypeUserGrant:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT
case feature.ImprovedPerformanceTypeOrgDomainVerified:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED
default:
return feature_pb.ImprovedPerformance(typ)
}
}
func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []feature.ImprovedPerformanceType {
if list == nil {
return nil
}
res := make([]feature.ImprovedPerformanceType, len(list))
for i, typ := range list {
res[i] = improvedPerformanceToDomain(typ)
}
return res
}
func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType {
switch typ {
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED:
return feature.ImprovedPerformanceTypeUnknown
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID:
return feature.ImprovedPerformanceTypeOrgByID
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT:
return feature.ImprovedPerformanceTypeProjectGrant
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT:
return feature.ImprovedPerformanceTypeProject
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT:
return feature.ImprovedPerformanceTypeUserGrant
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED:
return feature.ImprovedPerformanceTypeOrgDomainVerified
default:
return feature.ImprovedPerformanceTypeUnknown
}
}

View File

@ -0,0 +1,268 @@
package feature
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/feature"
"github.com/zitadel/zitadel/internal/query"
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
)
func Test_systemFeaturesToCommand(t *testing.T) {
arg := &feature_pb.SetSystemFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
OidcTriggerIntrospectionProjections: gu.Ptr(false),
OidcLegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
ImprovedPerformance: nil,
}
got := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
}
func Test_systemFeaturesToPb(t *testing.T) {
arg := &query.SystemFeatures{
Details: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(123, 0),
ResourceOwner: "SYSTEM",
},
LoginDefaultOrg: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
TriggerIntrospectionProjections: query.FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
LegacyIntrospection: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
UserSchema: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
Actions: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
TokenExchange: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: false,
},
ImprovedPerformance: query.FeatureSource[[]feature.ImprovedPerformanceType]{
Level: feature.LevelSystem,
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{Seconds: 123},
ResourceOwner: "SYSTEM",
},
LoginDefaultOrg: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{
Enabled: false,
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
},
OidcLegacyIntrospection: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
UserSchema: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
OidcTokenExchange: &feature_pb.FeatureFlag{
Enabled: false,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
Actions: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
}
func Test_instanceFeaturesToCommand(t *testing.T) {
arg := &feature_pb.SetInstanceFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
OidcTriggerIntrospectionProjections: gu.Ptr(false),
OidcLegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
}
func Test_instanceFeaturesToPb(t *testing.T) {
arg := &query.InstanceFeatures{
Details: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(123, 0),
ResourceOwner: "instance1",
},
LoginDefaultOrg: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
TriggerIntrospectionProjections: query.FeatureSource[bool]{
Level: feature.LevelUnspecified,
Value: false,
},
LegacyIntrospection: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
UserSchema: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
Actions: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
TokenExchange: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: false,
},
ImprovedPerformance: query.FeatureSource[[]feature.ImprovedPerformanceType]{
Level: feature.LevelSystem,
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{Seconds: 123},
ResourceOwner: "instance1",
},
LoginDefaultOrg: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{
Enabled: false,
Source: feature_pb.Source_SOURCE_UNSPECIFIED,
},
OidcLegacyIntrospection: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
UserSchema: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
Actions: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
OidcTokenExchange: &feature_pb.FeatureFlag{
Enabled: false,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
ImprovedPerformance: &feature_pb.ImprovedPerformanceFeatureFlag{
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)
}
func Test_featureLevelToSourcePb(t *testing.T) {
tests := []struct {
name string
level feature.Level
want feature_pb.Source
}{
{
name: "unspecified",
level: feature.LevelUnspecified,
want: feature_pb.Source_SOURCE_UNSPECIFIED,
},
{
name: "system",
level: feature.LevelSystem,
want: feature_pb.Source_SOURCE_SYSTEM,
},
{
name: "instance",
level: feature.LevelInstance,
want: feature_pb.Source_SOURCE_INSTANCE,
},
{
name: "org",
level: feature.LevelOrg,
want: feature_pb.Source_SOURCE_ORGANIZATION,
},
{
name: "project",
level: feature.LevelProject,
want: feature_pb.Source_SOURCE_PROJECT,
},
{
name: "app",
level: feature.LevelApp,
want: feature_pb.Source_SOURCE_APP,
},
{
name: "user",
level: feature.LevelUser,
want: feature_pb.Source_SOURCE_USER,
},
{
name: "unknown",
level: 99,
want: 99,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := featureLevelToSourcePb(tt.level)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,86 @@
package feature
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
)
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req))
if err != nil {
return nil, err
}
return &feature.SetSystemFeaturesResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) ResetSystemFeatures(ctx context.Context, req *feature.ResetSystemFeaturesRequest) (_ *feature.ResetSystemFeaturesResponse, err error) {
details, err := s.command.ResetSystemFeatures(ctx)
if err != nil {
return nil, err
}
return &feature.ResetSystemFeaturesResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFeaturesRequest) (_ *feature.GetSystemFeaturesResponse, err error) {
f, err := s.query.GetSystemFeatures(ctx)
if err != nil {
return nil, err
}
return systemFeaturesToPb(f), nil
}
func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) {
details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req))
if err != nil {
return nil, err
}
return &feature.SetInstanceFeaturesResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) ResetInstanceFeatures(ctx context.Context, req *feature.ResetInstanceFeaturesRequest) (_ *feature.ResetInstanceFeaturesResponse, err error) {
details, err := s.command.ResetInstanceFeatures(ctx)
if err != nil {
return nil, err
}
return &feature.ResetInstanceFeaturesResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) GetInstanceFeatures(ctx context.Context, req *feature.GetInstanceFeaturesRequest) (_ *feature.GetInstanceFeaturesResponse, err error) {
f, err := s.query.GetInstanceFeatures(ctx, req.GetInheritance())
if err != nil {
return nil, err
}
return instanceFeaturesToPb(f), nil
}
func (s *Server) SetOrganizationFeatures(ctx context.Context, req *feature.SetOrganizationFeaturesRequest) (_ *feature.SetOrganizationFeaturesResponse, err error) {
return nil, status.Errorf(codes.Unimplemented, "method SetOrganizationFeatures not implemented")
}
func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *feature.ResetOrganizationFeaturesRequest) (_ *feature.ResetOrganizationFeaturesResponse, err error) {
return nil, status.Errorf(codes.Unimplemented, "method ResetOrganizationFeatures not implemented")
}
func (s *Server) GetOrganizationFeatures(ctx context.Context, req *feature.GetOrganizationFeaturesRequest) (_ *feature.GetOrganizationFeaturesResponse, err error) {
return nil, status.Errorf(codes.Unimplemented, "method GetOrganizationFeatures not implemented")
}
func (s *Server) SetUserFeatures(ctx context.Context, req *feature.SetUserFeatureRequest) (_ *feature.SetUserFeaturesResponse, err error) {
return nil, status.Errorf(codes.Unimplemented, "method SetUserFeatures not implemented")
}
func (s *Server) ResetUserFeatures(ctx context.Context, req *feature.ResetUserFeaturesRequest) (_ *feature.ResetUserFeaturesResponse, err error) {
return nil, status.Errorf(codes.Unimplemented, "method ResetUserFeatures not implemented")
}
func (s *Server) GetUserFeatures(ctx context.Context, req *feature.GetUserFeaturesRequest) (_ *feature.GetUserFeaturesResponse, err error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserFeatures not implemented")
}

View File

@ -0,0 +1,499 @@
//go:build integration
package feature_test
import (
"context"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
)
var (
SystemCTX context.Context
IamCTX context.Context
OrgCTX context.Context
Tester *integration.Tester
Client feature.FeatureServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
OrgCTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
defer Tester.Done()
Client = Tester.Client.FeatureV2beta
return m.Run()
}())
}
func TestServer_SetSystemFeatures(t *testing.T) {
type args struct {
ctx context.Context
req *feature.SetSystemFeaturesRequest
}
tests := []struct {
name string
args args
want *feature.SetSystemFeaturesResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: IamCTX,
req: &feature.SetSystemFeaturesRequest{
OidcTriggerIntrospectionProjections: gu.Ptr(true),
},
},
wantErr: true,
},
{
name: "no changes error",
args: args{
ctx: SystemCTX,
req: &feature.SetSystemFeaturesRequest{},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: SystemCTX,
req: &feature.SetSystemFeaturesRequest{
OidcTriggerIntrospectionProjections: gu.Ptr(true),
},
},
want: &feature.SetSystemFeaturesResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: "SYSTEM",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Cleanup(func() {
// make sure we have a clean state after each test
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
require.NoError(t, err)
})
got, err := Client.SetSystemFeatures(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_ResetSystemFeatures(t *testing.T) {
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
})
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
want *feature.ResetSystemFeaturesResponse
wantErr bool
}{
{
name: "permission error",
ctx: IamCTX,
wantErr: true,
},
{
name: "success",
ctx: SystemCTX,
want: &feature.ResetSystemFeaturesResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: "SYSTEM",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ResetSystemFeatures(tt.ctx, &feature.ResetSystemFeaturesRequest{})
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_GetSystemFeatures(t *testing.T) {
type args struct {
ctx context.Context
req *feature.GetSystemFeaturesRequest
}
tests := []struct {
name string
prepare func(t *testing.T)
args args
want *feature.GetSystemFeaturesResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: IamCTX,
req: &feature.GetSystemFeaturesRequest{},
},
wantErr: true,
},
{
name: "nothing set",
args: args{
ctx: SystemCTX,
req: &feature.GetSystemFeaturesRequest{},
},
want: &feature.GetSystemFeaturesResponse{},
},
{
name: "some features",
prepare: func(t *testing.T) {
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
OidcTriggerIntrospectionProjections: gu.Ptr(false),
})
require.NoError(t, err)
},
args: args{
ctx: SystemCTX,
req: &feature.GetSystemFeaturesRequest{},
},
want: &feature.GetSystemFeaturesResponse{
LoginDefaultOrg: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_SYSTEM,
},
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_SYSTEM,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Cleanup(func() {
// make sure we have a clean state after each test
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
require.NoError(t, err)
})
if tt.prepare != nil {
tt.prepare(t)
}
got, err := Client.GetSystemFeatures(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
assertFeatureFlag(t, tt.want.Actions, got.Actions)
})
}
}
func TestServer_SetInstanceFeatures(t *testing.T) {
type args struct {
ctx context.Context
req *feature.SetInstanceFeaturesRequest
}
tests := []struct {
name string
args args
want *feature.SetInstanceFeaturesResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: OrgCTX,
req: &feature.SetInstanceFeaturesRequest{
OidcTriggerIntrospectionProjections: gu.Ptr(true),
},
},
wantErr: true,
},
{
name: "no changes error",
args: args{
ctx: IamCTX,
req: &feature.SetInstanceFeaturesRequest{},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: IamCTX,
req: &feature.SetInstanceFeaturesRequest{
OidcTriggerIntrospectionProjections: gu.Ptr(true),
},
},
want: &feature.SetInstanceFeaturesResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Cleanup(func() {
// make sure we have a clean state after each test
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
require.NoError(t, err)
})
got, err := Client.SetInstanceFeatures(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_ResetInstanceFeatures(t *testing.T) {
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
})
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
want *feature.ResetInstanceFeaturesResponse
wantErr bool
}{
{
name: "permission error",
ctx: OrgCTX,
wantErr: true,
},
{
name: "success",
ctx: IamCTX,
want: &feature.ResetInstanceFeaturesResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ResetInstanceFeatures(tt.ctx, &feature.ResetInstanceFeaturesRequest{})
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_GetInstanceFeatures(t *testing.T) {
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
OidcLegacyIntrospection: gu.Ptr(true),
})
require.NoError(t, err)
t.Cleanup(func() {
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
require.NoError(t, err)
})
type args struct {
ctx context.Context
req *feature.GetInstanceFeaturesRequest
}
tests := []struct {
name string
prepare func(t *testing.T)
args args
want *feature.GetInstanceFeaturesResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: OrgCTX,
req: &feature.GetInstanceFeaturesRequest{},
},
wantErr: true,
},
{
name: "defaults, no inheritance",
args: args{
ctx: IamCTX,
req: &feature.GetInstanceFeaturesRequest{},
},
want: &feature.GetInstanceFeaturesResponse{},
},
{
name: "defaults, inheritance",
args: args{
ctx: IamCTX,
req: &feature.GetInstanceFeaturesRequest{
Inheritance: true,
},
},
want: &feature.GetInstanceFeaturesResponse{
LoginDefaultOrg: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
OidcLegacyIntrospection: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_SYSTEM,
},
UserSchema: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
Actions: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
},
},
{
name: "some features, no inheritance",
prepare: func(t *testing.T) {
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
OidcTriggerIntrospectionProjections: gu.Ptr(false),
UserSchema: gu.Ptr(true),
Actions: gu.Ptr(true),
})
require.NoError(t, err)
},
args: args{
ctx: IamCTX,
req: &feature.GetInstanceFeaturesRequest{},
},
want: &feature.GetInstanceFeaturesResponse{
LoginDefaultOrg: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_INSTANCE,
},
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_INSTANCE,
},
UserSchema: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_INSTANCE,
},
Actions: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_INSTANCE,
},
},
},
{
name: "one feature, inheritance",
prepare: func(t *testing.T) {
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
LoginDefaultOrg: gu.Ptr(true),
})
require.NoError(t, err)
},
args: args{
ctx: IamCTX,
req: &feature.GetInstanceFeaturesRequest{
Inheritance: true,
},
},
want: &feature.GetInstanceFeaturesResponse{
LoginDefaultOrg: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_INSTANCE,
},
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
OidcLegacyIntrospection: &feature.FeatureFlag{
Enabled: true,
Source: feature.Source_SOURCE_SYSTEM,
},
UserSchema: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
Actions: &feature.FeatureFlag{
Enabled: false,
Source: feature.Source_SOURCE_UNSPECIFIED,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Cleanup(func() {
// make sure we have a clean state after each test
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
require.NoError(t, err)
})
if tt.prepare != nil {
tt.prepare(t)
}
got, err := Client.GetInstanceFeatures(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
})
}
}
func assertFeatureFlag(t *testing.T, expected, actual *feature.FeatureFlag) {
t.Helper()
assert.Equal(t, expected.GetEnabled(), actual.GetEnabled(), "enabled")
assert.Equal(t, expected.GetSource(), actual.GetSource(), "source")
}

View File

@ -0,0 +1,47 @@
package feature
import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
)
type Server struct {
feature.UnimplementedFeatureServiceServer
command *command.Commands
query *query.Queries
}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
feature.RegisterFeatureServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return feature.FeatureService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return feature.FeatureService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return feature.FeatureService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return feature.RegisterFeatureServiceHandler
}

View File

@ -8,7 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {

View File

@ -0,0 +1,72 @@
package object
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
)
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
details := &object.Details{
Sequence: objectDetail.Sequence,
ResourceOwner: objectDetail.ResourceOwner,
}
if !objectDetail.EventDate.IsZero() {
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
}
return details
}
func ToListDetails(response query.SearchResponse) *object.ListDetails {
details := &object.ListDetails{
TotalResult: response.Count,
ProcessedSequence: response.Sequence,
Timestamp: timestamppb.New(response.EventCreatedAt),
}
return details
}
func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) {
if query == nil {
return 0, 0, false
}
return query.Offset, uint64(query.Limit), query.Asc
}
func ResourceOwnerFromReq(ctx context.Context, req *object.RequestContext) string {
if req.GetInstance() {
return authz.GetInstance(ctx).InstanceID()
}
if req.GetOrgId() != "" {
return req.GetOrgId()
}
return authz.GetCtxData(ctx).OrgID
}
func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison {
switch method {
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS:
return query.TextEquals
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE:
return query.TextEqualsIgnoreCase
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH:
return query.TextStartsWith
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH_IGNORE_CASE:
return query.TextStartsWithIgnoreCase
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS:
return query.TextContains
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE:
return query.TextContainsIgnoreCase
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH:
return query.TextEndsWith
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH_IGNORE_CASE:
return query.TextEndsWithIgnoreCase
default:
return -1
}
}

View File

@ -15,7 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
)
func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) {

View File

@ -16,10 +16,10 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (

View File

@ -12,7 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
)
func Test_authRequestToPb(t *testing.T) {

View File

@ -8,7 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
)
var _ oidc_pb.OIDCServiceServer = (*Server)(nil)

View File

@ -0,0 +1,204 @@
package oidc
import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/op"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
)
func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) {
authRequest, err := s.query.AuthRequestByID(ctx, true, req.GetAuthRequestId(), true)
if err != nil {
logging.WithError(err).Error("query authRequest by ID")
return nil, err
}
return &oidc_pb.GetAuthRequestResponse{
AuthRequest: authRequestToPb(authRequest),
}, nil
}
func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest {
pba := &oidc_pb.AuthRequest{
Id: a.ID,
CreationDate: timestamppb.New(a.CreationDate),
ClientId: a.ClientID,
Scope: a.Scope,
RedirectUri: a.RedirectURI,
Prompt: promptsToPb(a.Prompt),
UiLocales: a.UiLocales,
LoginHint: a.LoginHint,
HintUserId: a.HintUserID,
}
if a.MaxAge != nil {
pba.MaxAge = durationpb.New(*a.MaxAge)
}
return pba
}
func promptsToPb(promps []domain.Prompt) []oidc_pb.Prompt {
out := make([]oidc_pb.Prompt, len(promps))
for i, p := range promps {
out[i] = promptToPb(p)
}
return out
}
func promptToPb(p domain.Prompt) oidc_pb.Prompt {
switch p {
case domain.PromptUnspecified:
return oidc_pb.Prompt_PROMPT_UNSPECIFIED
case domain.PromptNone:
return oidc_pb.Prompt_PROMPT_NONE
case domain.PromptLogin:
return oidc_pb.Prompt_PROMPT_LOGIN
case domain.PromptConsent:
return oidc_pb.Prompt_PROMPT_CONSENT
case domain.PromptSelectAccount:
return oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT
case domain.PromptCreate:
return oidc_pb.Prompt_PROMPT_CREATE
default:
return oidc_pb.Prompt_PROMPT_UNSPECIFIED
}
}
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
switch v := req.GetCallbackKind().(type) {
case *oidc_pb.CreateCallbackRequest_Error:
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
case *oidc_pb.CreateCallbackRequest_Session:
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
}
}
func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) {
details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError()))
if err != nil {
return nil, err
}
authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
callback, err := oidc.CreateErrorCallbackURL(authReq, errorReasonToOIDC(ae.GetError()), ae.GetErrorDescription(), ae.GetErrorUri(), s.op.Provider())
if err != nil {
return nil, err
}
return &oidc_pb.CreateCallbackResponse{
Details: object.DomainToDetailsPb(details),
CallbackUrl: callback,
}, nil
}
func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) {
details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true)
if err != nil {
return nil, err
}
authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure))
var callback string
if aar.ResponseType == domain.OIDCResponseTypeCode {
callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider())
} else {
callback, err = s.op.CreateTokenCallbackURL(ctx, authReq)
}
if err != nil {
return nil, err
}
return &oidc_pb.CreateCallbackResponse{
Details: object.DomainToDetailsPb(details),
CallbackUrl: callback,
}, nil
}
func errorReasonToDomain(errorReason oidc_pb.ErrorReason) domain.OIDCErrorReason {
switch errorReason {
case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED:
return domain.OIDCErrorReasonUnspecified
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
return domain.OIDCErrorReasonInvalidRequest
case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
return domain.OIDCErrorReasonUnauthorizedClient
case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
return domain.OIDCErrorReasonAccessDenied
case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
return domain.OIDCErrorReasonUnsupportedResponseType
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
return domain.OIDCErrorReasonInvalidScope
case oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
return domain.OIDCErrorReasonServerError
case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
return domain.OIDCErrorReasonTemporaryUnavailable
case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
return domain.OIDCErrorReasonInteractionRequired
case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
return domain.OIDCErrorReasonLoginRequired
case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
return domain.OIDCErrorReasonAccountSelectionRequired
case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
return domain.OIDCErrorReasonConsentRequired
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
return domain.OIDCErrorReasonInvalidRequestURI
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
return domain.OIDCErrorReasonInvalidRequestObject
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
return domain.OIDCErrorReasonRequestNotSupported
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
return domain.OIDCErrorReasonRequestURINotSupported
case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
return domain.OIDCErrorReasonRegistrationNotSupported
default:
return domain.OIDCErrorReasonUnspecified
}
}
func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
switch reason {
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
return "invalid_request"
case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
return "unauthorized_client"
case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
return "access_denied"
case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
return "unsupported_response_type"
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
return "invalid_scope"
case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
return "temporarily_unavailable"
case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
return "interaction_required"
case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
return "login_required"
case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
return "account_selection_required"
case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
return "consent_required"
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
return "invalid_request_uri"
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
return "invalid_request_object"
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
return "request_not_supported"
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
return "request_uri_not_supported"
case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
return "registration_not_supported"
case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED, oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
fallthrough
default:
return "server_error"
}
}

View File

@ -0,0 +1,258 @@
//go:build integration
package oidc_test
import (
"context"
"net/url"
"os"
"regexp"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
CTX context.Context
Tester *integration.Tester
Client oidc_pb.OIDCServiceClient
User *user.AddHumanUserResponse
)
const (
redirectURI = "oidcintegrationtest://callback"
redirectURIImplicit = "http://localhost:9999/callback"
logoutRedirectURI = "oidcintegrationtest://logged-out"
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
Client = Tester.Client.OIDCv2beta
CTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
User = Tester.CreateHumanUser(CTX)
return m.Run()
}())
}
func TestServer_GetAuthRequest(t *testing.T) {
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err)
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
require.NoError(t, err)
now := time.Now()
tests := []struct {
name string
AuthRequestID string
want *oidc_pb.GetAuthRequestResponse
wantErr bool
}{
{
name: "Not found",
AuthRequestID: "123",
wantErr: true,
},
{
name: "success",
AuthRequestID: authRequestID,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.GetAuthRequest(CTX, &oidc_pb.GetAuthRequestRequest{
AuthRequestId: tt.AuthRequestID,
})
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
authRequest := got.GetAuthRequest()
assert.NotNil(t, authRequest)
assert.Equal(t, authRequestID, authRequest.GetId())
assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second))
assert.Contains(t, authRequest.GetScope(), "openid")
})
}
}
func TestServer_CreateCallback(t *testing.T) {
project, err := Tester.CreateProject(CTX)
require.NoError(t, err)
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
require.NoError(t, err)
sessionResp, err := Tester.Client.SessionV2beta.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
},
},
},
})
require.NoError(t, err)
tests := []struct {
name string
req *oidc_pb.CreateCallbackRequest
AuthError string
want *oidc_pb.CreateCallbackResponse
wantURL *url.URL
wantErr bool
}{
{
name: "Not found",
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: "123",
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
wantErr: true,
},
{
name: "session not found",
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: "foo",
SessionToken: "bar",
},
},
},
wantErr: true,
},
{
name: "session token invalid",
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: "bar",
},
},
},
wantErr: true,
},
{
name: "fail callback",
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Error{
Error: &oidc_pb.AuthorizationError{
Error: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
ErrorDescription: gu.Ptr("nope"),
ErrorUri: gu.Ptr("https://example.com/docs"),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: regexp.QuoteMeta(`oidcintegrationtest://callback?error=access_denied&error_description=nope&error_uri=https%3A%2F%2Fexample.com%2Fdocs&state=state`),
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantErr: false,
},
{
name: "code callback",
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantErr: false,
},
{
name: "implicit",
req: &oidc_pb.CreateCallbackRequest{
AuthRequestId: func() string {
client, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
require.NoError(t, err)
authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit)
require.NoError(t, err)
return authRequestID
}(),
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
Session: &oidc_pb.Session{
SessionId: sessionResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
},
},
},
want: &oidc_pb.CreateCallbackResponse{
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateCallback(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
if tt.want != nil {
assert.Regexp(t, regexp.MustCompile(tt.want.CallbackUrl), got.GetCallbackUrl())
}
})
}
}

View File

@ -0,0 +1,150 @@
package oidc
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
)
func Test_authRequestToPb(t *testing.T) {
now := time.Now()
arg := &query.AuthRequest{
ID: "authID",
CreationDate: now,
ClientID: "clientID",
Scope: []string{"a", "b", "c"},
RedirectURI: "callbackURI",
Prompt: []domain.Prompt{
domain.PromptUnspecified,
domain.PromptNone,
domain.PromptLogin,
domain.PromptConsent,
domain.PromptSelectAccount,
domain.PromptCreate,
999,
},
UiLocales: []string{"en", "fi"},
LoginHint: gu.Ptr("foo@bar.com"),
MaxAge: gu.Ptr(time.Minute),
HintUserID: gu.Ptr("userID"),
}
want := &oidc_pb.AuthRequest{
Id: "authID",
CreationDate: timestamppb.New(now),
ClientId: "clientID",
RedirectUri: "callbackURI",
Prompt: []oidc_pb.Prompt{
oidc_pb.Prompt_PROMPT_UNSPECIFIED,
oidc_pb.Prompt_PROMPT_NONE,
oidc_pb.Prompt_PROMPT_LOGIN,
oidc_pb.Prompt_PROMPT_CONSENT,
oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT,
oidc_pb.Prompt_PROMPT_CREATE,
oidc_pb.Prompt_PROMPT_UNSPECIFIED,
},
UiLocales: []string{"en", "fi"},
Scope: []string{"a", "b", "c"},
LoginHint: gu.Ptr("foo@bar.com"),
MaxAge: durationpb.New(time.Minute),
HintUserId: gu.Ptr("userID"),
}
got := authRequestToPb(arg)
if !proto.Equal(want, got) {
t.Errorf("authRequestToPb() =\n%v\nwant\n%v\n", got, want)
}
}
func Test_errorReasonToOIDC(t *testing.T) {
tests := []struct {
reason oidc_pb.ErrorReason
want string
}{
{
reason: oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED,
want: "server_error",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST,
want: "invalid_request",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT,
want: "unauthorized_client",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
want: "access_denied",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE,
want: "unsupported_response_type",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE,
want: "invalid_scope",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR,
want: "server_error",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE,
want: "temporarily_unavailable",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED,
want: "interaction_required",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED,
want: "login_required",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED,
want: "account_selection_required",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED,
want: "consent_required",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI,
want: "invalid_request_uri",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT,
want: "invalid_request_object",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED,
want: "request_not_supported",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED,
want: "request_uri_not_supported",
},
{
reason: oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED,
want: "registration_not_supported",
},
{
reason: 99999,
want: "server_error",
},
}
for _, tt := range tests {
t.Run(tt.reason.String(), func(t *testing.T) {
got := errorReasonToOIDC(tt.reason)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,59 @@
package oidc
import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
)
var _ oidc_pb.OIDCServiceServer = (*Server)(nil)
type Server struct {
oidc_pb.UnimplementedOIDCServiceServer
command *command.Commands
query *query.Queries
op *oidc.Server
externalSecure bool
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
op *oidc.Server,
externalSecure bool,
) *Server {
return &Server{
command: command,
query: query,
op: op,
externalSecure: externalSecure,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
oidc_pb.RegisterOIDCServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return oidc_pb.OIDCService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return oidc_pb.OIDCService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return oidc_pb.OIDCService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return oidc_pb.RegisterOIDCServiceHandler
}

View File

@ -7,7 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/zerrors"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
)
func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) {

View File

@ -14,8 +14,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (

View File

@ -12,9 +12,9 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_addOrganizationRequestToCommand(t *testing.T) {

View File

@ -8,7 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
)
var _ org.OrganizationServiceServer = (*Server)(nil)

View File

@ -0,0 +1,83 @@
package org
import (
"context"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/zerrors"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
)
func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) {
orgSetup, err := addOrganizationRequestToCommand(request)
if err != nil {
return nil, err
}
createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false)
if err != nil {
return nil, err
}
return createdOrganizationToPb(createdOrg)
}
func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) {
admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins())
if err != nil {
return nil, err
}
return &command.OrgSetup{
Name: request.GetName(),
CustomDomain: "",
Admins: admins,
}, nil
}
func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) {
admins = make([]*command.OrgSetupAdmin, len(requestAdmins))
for i, admin := range requestAdmins {
admins[i], err = addOrganizationRequestAdminToCommand(admin)
if err != nil {
return nil, err
}
}
return admins, nil
}
func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) {
switch a := admin.GetUserType().(type) {
case *org.AddOrganizationRequest_Admin_UserId:
return &command.OrgSetupAdmin{
ID: a.UserId,
Roles: admin.GetRoles(),
}, nil
case *org.AddOrganizationRequest_Admin_Human:
human, err := user.AddUserRequestToAddHuman(a.Human)
if err != nil {
return nil, err
}
return &command.OrgSetupAdmin{
Human: human,
Roles: admin.GetRoles(),
}, nil
default:
return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a)
}
}
func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) {
admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins))
for i, admin := range createdOrg.CreatedAdmins {
admins[i] = &org.AddOrganizationResponse_CreatedAdmin{
UserId: admin.ID,
EmailCode: admin.EmailCode,
PhoneCode: admin.PhoneCode,
}
}
return &org.AddOrganizationResponse{
Details: object.DomainToDetailsPb(createdOrg.ObjectDetails),
OrganizationId: createdOrg.ObjectDetails.ResourceOwner,
CreatedAdmins: admins,
}, nil
}

View File

@ -0,0 +1,207 @@
//go:build integration
package org_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
var (
CTX context.Context
Tester *integration.Tester
Client org.OrganizationServiceClient
User *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
Client = Tester.Client.OrgV2beta
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
User = Tester.CreateHumanUser(CTX)
return m.Run()
}())
}
func TestServer_AddOrganization(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
tests := []struct {
name string
ctx context.Context
req *org.AddOrganizationRequest
want *org.AddOrganizationResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &org.AddOrganizationRequest{
Name: "name",
Admins: nil,
},
wantErr: true,
},
{
name: "empty name",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: "",
Admins: nil,
},
wantErr: true,
},
{
name: "invalid admin type",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
Admins: []*org.AddOrganizationRequest_Admin{
{},
},
},
wantErr: true,
},
{
name: "admin with init",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_Human{
Human: &user_v2beta.AddHumanUserRequest{
Profile: &user_v2beta.SetHumanProfile{
GivenName: "firstname",
FamilyName: "lastname",
},
Email: &user_v2beta.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user_v2beta.SetHumanEmail_ReturnCode{
ReturnCode: &user_v2beta.ReturnEmailVerificationCode{},
},
},
},
},
},
},
},
want: &org.AddOrganizationResponse{
OrganizationId: integration.NotEmpty,
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
{
UserId: integration.NotEmpty,
EmailCode: gu.Ptr(integration.NotEmpty),
PhoneCode: nil,
},
},
},
},
{
name: "existing user and new human with idp",
ctx: CTX,
req: &org.AddOrganizationRequest{
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()},
},
{
UserType: &org.AddOrganizationRequest_Admin_Human{
Human: &user_v2beta.AddHumanUserRequest{
Profile: &user_v2beta.SetHumanProfile{
GivenName: "firstname",
FamilyName: "lastname",
},
Email: &user_v2beta.SetHumanEmail{
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
Verification: &user_v2beta.SetHumanEmail_IsVerified{
IsVerified: true,
},
},
IdpLinks: []*user_v2beta.IDPLink{
{
IdpId: idpID,
UserId: "userID",
UserName: "username",
},
},
},
},
},
},
},
want: &org.AddOrganizationResponse{
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
// a single admin is expected, because the first provided already exists
{
UserId: integration.NotEmpty,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddOrganization(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// check details
assert.NotZero(t, got.GetDetails().GetSequence())
gotCD := got.GetDetails().GetChangeDate().AsTime()
now := time.Now()
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEmpty(t, got.GetDetails().GetResourceOwner())
// organization id must be the same as the resourceOwner
assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId())
// check the admins
require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins()))
for i, admin := range tt.want.GetCreatedAdmins() {
gotAdmin := got.GetCreatedAdmins()[i]
assertCreatedAdmin(t, admin, gotAdmin)
}
})
}
}
func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) {
if expected.GetUserId() != "" {
assert.NotEmpty(t, got.GetUserId())
} else {
assert.Empty(t, got.GetUserId())
}
if expected.GetEmailCode() != "" {
assert.NotEmpty(t, got.GetEmailCode())
} else {
assert.Empty(t, got.GetEmailCode())
}
if expected.GetPhoneCode() != "" {
assert.NotEmpty(t, got.GetPhoneCode())
} else {
assert.Empty(t, got.GetPhoneCode())
}
}

View File

@ -0,0 +1,172 @@
package org
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func Test_addOrganizationRequestToCommand(t *testing.T) {
type args struct {
request *org.AddOrganizationRequest
}
tests := []struct {
name string
args args
want *command.OrgSetup
wantErr error
}{
{
name: "nil user",
args: args{
request: &org.AddOrganizationRequest{
Name: "name",
Admins: []*org.AddOrganizationRequest_Admin{
{},
},
},
},
wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil),
},
{
name: "user ID",
args: args{
request: &org.AddOrganizationRequest{
Name: "name",
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_UserId{
UserId: "userID",
},
Roles: nil,
},
},
},
},
want: &command.OrgSetup{
Name: "name",
CustomDomain: "",
Admins: []*command.OrgSetupAdmin{
{
ID: "userID",
},
},
},
},
{
name: "human user",
args: args{
request: &org.AddOrganizationRequest{
Name: "name",
Admins: []*org.AddOrganizationRequest_Admin{
{
UserType: &org.AddOrganizationRequest_Admin_Human{
Human: &user.AddHumanUserRequest{
Profile: &user.SetHumanProfile{
GivenName: "firstname",
FamilyName: "lastname",
},
Email: &user.SetHumanEmail{
Email: "email@test.com",
},
},
},
Roles: nil,
},
},
},
},
want: &command.OrgSetup{
Name: "name",
CustomDomain: "",
Admins: []*command.OrgSetupAdmin{
{
Human: &command.AddHuman{
Username: "email@test.com",
FirstName: "firstname",
LastName: "lastname",
Email: command.Email{
Address: "email@test.com",
},
Metadata: make([]*command.AddMetadataEntry, 0),
Links: make([]*command.AddLink, 0),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := addOrganizationRequestToCommand(tt.args.request)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_createdOrganizationToPb(t *testing.T) {
now := time.Now()
type args struct {
createdOrg *command.CreatedOrg
}
tests := []struct {
name string
args args
want *org.AddOrganizationResponse
wantErr error
}{
{
name: "human user with phone and email code",
args: args{
createdOrg: &command.CreatedOrg{
ObjectDetails: &domain.ObjectDetails{
Sequence: 1,
EventDate: now,
ResourceOwner: "orgID",
},
CreatedAdmins: []*command.CreatedOrgAdmin{
{
ID: "id",
EmailCode: gu.Ptr("emailCode"),
PhoneCode: gu.Ptr("phoneCode"),
},
},
},
},
want: &org.AddOrganizationResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.New(now),
ResourceOwner: "orgID",
},
OrganizationId: "orgID",
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
{
UserId: "id",
EmailCode: gu.Ptr("emailCode"),
PhoneCode: gu.Ptr("phoneCode"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := createdOrganizationToPb(tt.args.createdOrg)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,55 @@
package org
import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
)
var _ org.OrganizationServiceServer = (*Server)(nil)
type Server struct {
org.UnimplementedOrganizationServiceServer
command *command.Commands
query *query.Queries
checkPermission domain.PermissionCheck
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
checkPermission domain.PermissionCheck,
) *Server {
return &Server{
command: command,
query: query,
checkPermission: checkPermission,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
org.RegisterOrganizationServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return org.OrganizationService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return org.OrganizationService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return org.OrganizationService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return org.RegisterOrganizationServiceHandler
}

View File

@ -29,6 +29,8 @@ func ActivityInterceptor() grpc.UnaryServerInterceptor {
var resourcePrefixes = []string{
"/zitadel.management.v1.ManagementService/",
"/zitadel.admin.v1.AdminService/",
"/zitadel.user.v2.UserService/",
"/zitadel.settings.v2.SettingsService/",
"/zitadel.user.v2beta.UserService/",
"/zitadel.settings.v2beta.SettingsService/",
"/zitadel.auth.v1.AuthService/",

View File

@ -131,7 +131,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -153,7 +153,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -181,7 +181,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -211,7 +211,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
@ -240,7 +240,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
@ -264,7 +264,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -293,7 +293,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Second,
@ -321,7 +321,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Minute,
@ -349,7 +349,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
@ -377,7 +377,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
@ -406,7 +406,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
@ -435,7 +435,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target1",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -443,7 +443,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target2",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -451,7 +451,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target3",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -493,7 +493,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target1",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -501,7 +501,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target2",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
@ -509,7 +509,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target3",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
@ -687,7 +687,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
@ -716,7 +716,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "response./zitadel.session.v2beta.SessionService/SetSession",
ExecutionID: "response./zitadel.session.v2.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,

View File

@ -7,7 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
var _ session.SessionServiceServer = (*Server)(nil)

View File

@ -18,7 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
var (

View File

@ -23,9 +23,9 @@ import (
"github.com/zitadel/zitadel/internal/integration"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
@ -860,7 +860,7 @@ func TestServer_SetSession_expired(t *testing.T) {
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)
_, err = Tester.Client.SessionV2.SetSession(CTX, &session.SetSessionRequest{
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Lifetime: durationpb.New(20 * time.Second),
})
@ -944,7 +944,7 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
require.Error(t, err)
require.Nil(t, sessionResp)
}
@ -953,7 +953,7 @@ func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
ctx := Tester.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
@ -966,17 +966,17 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
// test session token works
ctx := Tester.WithAuthorizationToken(context.Background(), token)
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
//terminate the session and test it does not work anymore
_, err = Tester.Client.SessionV2.DeleteSession(CTX, &session.DeleteSessionRequest{
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: id,
SessionToken: gu.Ptr(token),
})
require.NoError(t, err)
ctx = Tester.WithAuthorizationToken(context.Background(), token)
_, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
_, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
}
@ -985,12 +985,12 @@ func Test_ZITADEL_API_session_expired(t *testing.T) {
// test session token works
ctx := Tester.WithAuthorizationToken(context.Background(), token)
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
require.Nil(t, sessionResp)
}

View File

@ -18,8 +18,8 @@ import (
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
)
var (

View File

@ -0,0 +1,51 @@
package session
import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
)
var _ session.SessionServiceServer = (*Server)(nil)
type Server struct {
session.UnimplementedSessionServiceServer
command *command.Commands
query *query.Queries
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
session.RegisterSessionServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return session.SessionService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return session.SessionService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return session.SessionService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return session.RegisterSessionServiceHandler
}

View File

@ -0,0 +1,500 @@
package session
import (
"context"
"net"
"net/http"
"time"
"github.com/muhlemmer/gu"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
)
var (
timestampComparisons = map[objpb.TimestampQueryMethod]query.TimestampComparison{
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS: query.TimestampEquals,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER: query.TimestampGreater,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS: query.TimestampGreaterOrEquals,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS: query.TimestampLess,
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS: query.TimestampLessOrEquals,
}
)
func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) {
res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken())
if err != nil {
return nil, err
}
return &session.GetSessionResponse{
Session: sessionToPb(res),
}, nil
}
func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) {
queries, err := listSessionsRequestToQuery(ctx, req)
if err != nil {
return nil, err
}
sessions, err := s.query.SearchSessions(ctx, queries)
if err != nil {
return nil, err
}
return &session.ListSessionsResponse{
Details: object.ToListDetails(sessions.SearchResponse),
Sessions: sessionsToPb(sessions.Sessions),
}, nil
}
func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) {
checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req)
if err != nil {
return nil, err
}
challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
if err != nil {
return nil, err
}
set, err := s.command.CreateSession(ctx, cmds, metadata, userAgent, lifetime)
if err != nil {
return nil, err
}
return &session.CreateSessionResponse{
Details: object.DomainToDetailsPb(set.ObjectDetails),
SessionId: set.ID,
SessionToken: set.NewToken,
Challenges: challengeResponse,
}, nil
}
func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest) (*session.SetSessionResponse, error) {
checks, err := s.setSessionRequestToCommand(ctx, req)
if err != nil {
return nil, err
}
challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
if err != nil {
return nil, err
}
set, err := s.command.UpdateSession(ctx, req.GetSessionId(), cmds, req.GetMetadata(), req.GetLifetime().AsDuration())
if err != nil {
return nil, err
}
return &session.SetSessionResponse{
Details: object.DomainToDetailsPb(set.ObjectDetails),
SessionToken: set.NewToken,
Challenges: challengeResponse,
}, nil
}
func (s *Server) DeleteSession(ctx context.Context, req *session.DeleteSessionRequest) (*session.DeleteSessionResponse, error) {
details, err := s.command.TerminateSession(ctx, req.GetSessionId(), req.GetSessionToken())
if err != nil {
return nil, err
}
return &session.DeleteSessionResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func sessionsToPb(sessions []*query.Session) []*session.Session {
s := make([]*session.Session, len(sessions))
for i, session := range sessions {
s[i] = sessionToPb(session)
}
return s
}
func sessionToPb(s *query.Session) *session.Session {
return &session.Session{
Id: s.ID,
CreationDate: timestamppb.New(s.CreationDate),
ChangeDate: timestamppb.New(s.ChangeDate),
Sequence: s.Sequence,
Factors: factorsToPb(s),
Metadata: s.Metadata,
UserAgent: userAgentToPb(s.UserAgent),
ExpirationDate: expirationToPb(s.Expiration),
}
}
func userAgentToPb(ua domain.UserAgent) *session.UserAgent {
if ua.IsEmpty() {
return nil
}
out := &session.UserAgent{
FingerprintId: ua.FingerprintID,
Description: ua.Description,
}
if ua.IP != nil {
out.Ip = gu.Ptr(ua.IP.String())
}
if ua.Header == nil {
return out
}
out.Header = make(map[string]*session.UserAgent_HeaderValues, len(ua.Header))
for k, v := range ua.Header {
out.Header[k] = &session.UserAgent_HeaderValues{
Values: v,
}
}
return out
}
func expirationToPb(expiration time.Time) *timestamppb.Timestamp {
if expiration.IsZero() {
return nil
}
return timestamppb.New(expiration)
}
func factorsToPb(s *query.Session) *session.Factors {
user := userFactorToPb(s.UserFactor)
if user == nil {
return nil
}
return &session.Factors{
User: user,
Password: passwordFactorToPb(s.PasswordFactor),
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
Intent: intentFactorToPb(s.IntentFactor),
Totp: totpFactorToPb(s.TOTPFactor),
OtpSms: otpFactorToPb(s.OTPSMSFactor),
OtpEmail: otpFactorToPb(s.OTPEmailFactor),
}
}
func passwordFactorToPb(factor query.SessionPasswordFactor) *session.PasswordFactor {
if factor.PasswordCheckedAt.IsZero() {
return nil
}
return &session.PasswordFactor{
VerifiedAt: timestamppb.New(factor.PasswordCheckedAt),
}
}
func intentFactorToPb(factor query.SessionIntentFactor) *session.IntentFactor {
if factor.IntentCheckedAt.IsZero() {
return nil
}
return &session.IntentFactor{
VerifiedAt: timestamppb.New(factor.IntentCheckedAt),
}
}
func webAuthNFactorToPb(factor query.SessionWebAuthNFactor) *session.WebAuthNFactor {
if factor.WebAuthNCheckedAt.IsZero() {
return nil
}
return &session.WebAuthNFactor{
VerifiedAt: timestamppb.New(factor.WebAuthNCheckedAt),
UserVerified: factor.UserVerified,
}
}
func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor {
if factor.TOTPCheckedAt.IsZero() {
return nil
}
return &session.TOTPFactor{
VerifiedAt: timestamppb.New(factor.TOTPCheckedAt),
}
}
func otpFactorToPb(factor query.SessionOTPFactor) *session.OTPFactor {
if factor.OTPCheckedAt.IsZero() {
return nil
}
return &session.OTPFactor{
VerifiedAt: timestamppb.New(factor.OTPCheckedAt),
}
}
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
return nil
}
return &session.UserFactor{
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
Id: factor.UserID,
LoginName: factor.LoginName,
DisplayName: factor.DisplayName,
OrganizationId: factor.ResourceOwner,
}
}
func listSessionsRequestToQuery(ctx context.Context, req *session.ListSessionsRequest) (*query.SessionsSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := sessionQueriesToQuery(ctx, req.GetQueries())
if err != nil {
return nil, err
}
return &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToSessionColumn(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func sessionQueriesToQuery(ctx context.Context, queries []*session.SearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries)+1)
for i, v := range queries {
q[i], err = sessionQueryToQuery(v)
if err != nil {
return nil, err
}
}
creatorQuery, err := query.NewSessionCreatorSearchQuery(authz.GetCtxData(ctx).UserID)
if err != nil {
return nil, err
}
q[len(queries)] = creatorQuery
return q, nil
}
func sessionQueryToQuery(sq *session.SearchQuery) (query.SearchQuery, error) {
switch q := sq.Query.(type) {
case *session.SearchQuery_IdsQuery:
return idsQueryToQuery(q.IdsQuery)
case *session.SearchQuery_UserIdQuery:
return query.NewUserIDSearchQuery(q.UserIdQuery.GetId())
case *session.SearchQuery_CreationDateQuery:
return creationDateQueryToQuery(q.CreationDateQuery)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid")
}
}
func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) {
return query.NewSessionIDsSearchQuery(q.Ids)
}
func creationDateQueryToQuery(q *session.CreationDateQuery) (query.SearchQuery, error) {
comparison := timestampComparisons[q.GetMethod()]
return query.NewCreationDateQuery(q.GetCreationDate().AsTime(), comparison)
}
func fieldNameToSessionColumn(field session.SessionFieldName) query.Column {
switch field {
case session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE:
return query.SessionColumnCreationDate
case session.SessionFieldName_SESSION_FIELD_NAME_UNSPECIFIED:
// Handle all remaining cases so the linter succeeds
return query.Column{}
default:
return query.Column{}
}
}
func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, time.Duration, error) {
checks, err := s.checksToCommand(ctx, req.Checks)
if err != nil {
return nil, nil, nil, 0, err
}
return checks, req.GetMetadata(), userAgentToCommand(req.GetUserAgent()), req.GetLifetime().AsDuration(), nil
}
func userAgentToCommand(userAgent *session.UserAgent) *domain.UserAgent {
if userAgent == nil {
return nil
}
out := &domain.UserAgent{
FingerprintID: userAgent.FingerprintId,
IP: net.ParseIP(userAgent.GetIp()),
Description: userAgent.Description,
}
if len(userAgent.Header) > 0 {
out.Header = make(http.Header, len(userAgent.Header))
for k, values := range userAgent.Header {
out.Header[k] = values.GetValues()
}
}
return out
}
func (s *Server) setSessionRequestToCommand(ctx context.Context, req *session.SetSessionRequest) ([]command.SessionCommand, error) {
checks, err := s.checksToCommand(ctx, req.Checks)
if err != nil {
return nil, err
}
return checks, nil
}
func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([]command.SessionCommand, error) {
checkUser, err := userCheck(checks.GetUser())
if err != nil {
return nil, err
}
sessionChecks := make([]command.SessionCommand, 0, 7)
if checkUser != nil {
user, err := checkUser.search(ctx, s.query)
if err != nil {
return nil, err
}
if !user.State.IsEnabled() {
return nil, zerrors.ThrowPreconditionFailed(nil, "SESSION-Gj4ko", "Errors.User.NotActive")
}
var preferredLanguage *language.Tag
if user.Human != nil && !user.Human.PreferredLanguage.IsRoot() {
preferredLanguage = &user.Human.PreferredLanguage
}
sessionChecks = append(sessionChecks, command.CheckUser(user.ID, user.ResourceOwner, preferredLanguage))
}
if password := checks.GetPassword(); password != nil {
sessionChecks = append(sessionChecks, command.CheckPassword(password.GetPassword()))
}
if intent := checks.GetIdpIntent(); intent != nil {
sessionChecks = append(sessionChecks, command.CheckIntent(intent.GetIdpIntentId(), intent.GetIdpIntentToken()))
}
if passkey := checks.GetWebAuthN(); passkey != nil {
sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData()))
}
if totp := checks.GetTotp(); totp != nil {
sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetCode()))
}
if otp := checks.GetOtpSms(); otp != nil {
sessionChecks = append(sessionChecks, command.CheckOTPSMS(otp.GetCode()))
}
if otp := checks.GetOtpEmail(); otp != nil {
sessionChecks = append(sessionChecks, command.CheckOTPEmail(otp.GetCode()))
}
return sessionChecks, nil
}
func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand, error) {
if challenges == nil {
return nil, cmds, nil
}
resp := new(session.Challenges)
if req := challenges.GetWebAuthN(); req != nil {
challenge, cmd := s.createWebAuthNChallengeCommand(req)
resp.WebAuthN = challenge
cmds = append(cmds, cmd)
}
if req := challenges.GetOtpSms(); req != nil {
challenge, cmd := s.createOTPSMSChallengeCommand(req)
resp.OtpSms = challenge
cmds = append(cmds, cmd)
}
if req := challenges.GetOtpEmail(); req != nil {
challenge, cmd, err := s.createOTPEmailChallengeCommand(req)
if err != nil {
return nil, nil, err
}
resp.OtpEmail = challenge
cmds = append(cmds, cmd)
}
return resp, cmds, nil
}
func (s *Server) createWebAuthNChallengeCommand(req *session.RequestChallenges_WebAuthN) (*session.Challenges_WebAuthN, command.SessionCommand) {
challenge := &session.Challenges_WebAuthN{
PublicKeyCredentialRequestOptions: new(structpb.Struct),
}
userVerification := userVerificationRequirementToDomain(req.GetUserVerificationRequirement())
return challenge, s.command.CreateWebAuthNChallenge(userVerification, req.GetDomain(), challenge.PublicKeyCredentialRequestOptions)
}
func userVerificationRequirementToDomain(req session.UserVerificationRequirement) domain.UserVerificationRequirement {
switch req {
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED:
return domain.UserVerificationRequirementUnspecified
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED:
return domain.UserVerificationRequirementRequired
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED:
return domain.UserVerificationRequirementPreferred
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED:
return domain.UserVerificationRequirementDiscouraged
default:
return domain.UserVerificationRequirementUnspecified
}
}
func (s *Server) createOTPSMSChallengeCommand(req *session.RequestChallenges_OTPSMS) (*string, command.SessionCommand) {
if req.GetReturnCode() {
challenge := new(string)
return challenge, s.command.CreateOTPSMSChallengeReturnCode(challenge)
}
return nil, s.command.CreateOTPSMSChallenge()
}
func (s *Server) createOTPEmailChallengeCommand(req *session.RequestChallenges_OTPEmail) (*string, command.SessionCommand, error) {
switch t := req.GetDeliveryType().(type) {
case *session.RequestChallenges_OTPEmail_SendCode_:
cmd, err := s.command.CreateOTPEmailChallengeURLTemplate(t.SendCode.GetUrlTemplate())
if err != nil {
return nil, nil, err
}
return nil, cmd, nil
case *session.RequestChallenges_OTPEmail_ReturnCode_:
challenge := new(string)
return challenge, s.command.CreateOTPEmailChallengeReturnCode(challenge), nil
case nil:
return nil, s.command.CreateOTPEmailChallenge(), nil
default:
return nil, nil, zerrors.ThrowUnimplementedf(nil, "SESSION-k3ng0", "delivery_type oneOf %T in OTPEmailChallenge not implemented", t)
}
}
func userCheck(user *session.CheckUser) (userSearch, error) {
if user == nil {
return nil, nil
}
switch s := user.GetSearch().(type) {
case *session.CheckUser_UserId:
return userByID(s.UserId), nil
case *session.CheckUser_LoginName:
return userByLoginName(s.LoginName)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", s)
}
}
type userSearch interface {
search(ctx context.Context, q *query.Queries) (*query.User, error)
}
func userByID(userID string) userSearch {
return userSearchByID{userID}
}
func userByLoginName(loginName string) (userSearch, error) {
return userSearchByLoginName{loginName}, nil
}
type userSearchByID struct {
id string
}
func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.User, error) {
return q.GetUserByID(ctx, true, u.id)
}
type userSearchByLoginName struct {
loginName string
}
func (u userSearchByLoginName) search(ctx context.Context, q *query.Queries) (*query.User, error) {
return q.GetUserByLoginName(ctx, true, u.loginName)
}

View File

@ -0,0 +1,996 @@
//go:build integration
package session_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var (
CTX context.Context
IAMOwnerCTX context.Context
Tester *integration.Tester
Client session.SessionServiceClient
User *user.AddHumanUserResponse
DeactivatedUser *user.AddHumanUserResponse
LockedUser *user.AddHumanUserResponse
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
Client = Tester.Client.SessionV2beta
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
User = createFullUser(CTX)
DeactivatedUser = createDeactivatedUser(CTX)
LockedUser = createLockedUser(CTX)
return m.Run()
}())
}
func createFullUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Tester.CreateHumanUser(ctx)
Tester.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetEmailCode(),
})
Tester.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetPhoneCode(),
})
Tester.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false)
Tester.RegisterUserPasskey(ctx, userResp.GetUserId())
return userResp
}
func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Tester.CreateHumanUser(ctx)
_, err := Tester.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("deactivate human user")
return userResp
}
func createLockedUser(ctx context.Context) *user.AddHumanUserResponse {
userResp := Tester.CreateHumanUser(ctx)
_, err := Tester.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()})
logging.OnError(err).Fatal("lock human user")
return userResp
}
func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, userID string, factors ...wantFactor) *session.Session {
t.Helper()
require.NotEmpty(t, id)
require.NotEmpty(t, token)
resp, err := Client.GetSession(CTX, &session.GetSessionRequest{
SessionId: id,
SessionToken: &token,
})
require.NoError(t, err)
s := resp.GetSession()
assert.Equal(t, id, s.GetId())
assert.WithinRange(t, s.GetCreationDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.Equal(t, sequence, s.GetSequence())
assert.Equal(t, metadata, s.GetMetadata())
if !proto.Equal(userAgent, s.GetUserAgent()) {
t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent)
}
if expirationWindow == 0 {
assert.Nil(t, s.GetExpirationDate())
} else {
assert.WithinRange(t, s.GetExpirationDate().AsTime(), time.Now().Add(-expirationWindow), time.Now().Add(expirationWindow))
}
verifyFactors(t, s.GetFactors(), window, userID, factors)
return s
}
type wantFactor int
const (
wantUserFactor wantFactor = iota
wantPasswordFactor
wantWebAuthNFactor
wantWebAuthNFactorUserVerified
wantTOTPFactor
wantIntentFactor
wantOTPSMSFactor
wantOTPEmailFactor
)
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, userID string, want []wantFactor) {
for _, w := range want {
switch w {
case wantUserFactor:
uf := factors.GetUser()
assert.NotNil(t, uf)
assert.WithinRange(t, uf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.Equal(t, userID, uf.GetId())
case wantPasswordFactor:
pf := factors.GetPassword()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantWebAuthNFactor:
pf := factors.GetWebAuthN()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.False(t, pf.GetUserVerified())
case wantWebAuthNFactorUserVerified:
pf := factors.GetWebAuthN()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.True(t, pf.GetUserVerified())
case wantTOTPFactor:
pf := factors.GetTotp()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantIntentFactor:
pf := factors.GetIntent()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantOTPSMSFactor:
pf := factors.GetOtpSms()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantOTPEmailFactor:
pf := factors.GetOtpEmail()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
}
}
}
func TestServer_CreateSession(t *testing.T) {
tests := []struct {
name string
req *session.CreateSessionRequest
want *session.CreateSessionResponse
wantErr bool
wantFactors []wantFactor
wantUserAgent *session.UserAgent
wantExpirationWindow time.Duration
}{
{
name: "empty session",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "user agent",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantUserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("Description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
},
{
name: "negative lifetime",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
Lifetime: durationpb.New(-5 * time.Minute),
},
wantErr: true,
},
{
name: "lifetime",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
Lifetime: durationpb.New(5 * time.Minute),
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantExpirationWindow: 5 * time.Minute,
},
{
name: "with user",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
Metadata: map[string][]byte{"foo": []byte("bar")},
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantFactors: []wantFactor{wantUserFactor},
},
{
name: "deactivated user",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: DeactivatedUser.GetUserId(),
},
},
},
},
wantErr: true,
},
{
name: "locked user",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: LockedUser.GetUserId(),
},
},
},
},
wantErr: true,
},
{
name: "password without user error",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
Password: &session.CheckPassword{
Password: "Difficult",
},
},
},
wantErr: true,
},
{
name: "passkey without user error",
req: &session.CreateSessionRequest{
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
},
wantErr: true,
},
{
name: "passkey without domain (not registered) error",
req: &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateSession(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...)
})
}
}
func TestServer_CreateSession_lock_user(t *testing.T) {
// create a separate org so we don't interfere with any other test
org := Tester.CreateOrganization(IAMOwnerCTX,
fmt.Sprintf("TestServer_CreateSession_lock_user_%d", time.Now().UnixNano()),
fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
)
userID := org.CreatedAdmins[0].GetUserId()
Tester.SetUserPassword(IAMOwnerCTX, userID, integration.UserPassword, false)
// enable password lockout
maxAttempts := 2
ctxOrg := metadata.AppendToOutgoingContext(IAMOwnerCTX, "x-zitadel-orgid", org.GetOrganizationId())
_, err := Tester.Client.Mgmt.AddCustomLockoutPolicy(ctxOrg, &mgmt.AddCustomLockoutPolicyRequest{
MaxPasswordAttempts: uint32(maxAttempts),
})
require.NoError(t, err)
for i := 0; i <= maxAttempts; i++ {
_, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userID,
},
},
Password: &session.CheckPassword{
Password: "invalid",
},
},
})
assert.Error(t, err)
statusCode := status.Code(err)
expectedCode := codes.InvalidArgument
// as soon as we hit the limit the user is locked and following request will
// already deny any check with a precondition failed since the user is locked
if i >= maxAttempts {
expectedCode = codes.FailedPrecondition
}
assert.Equal(t, expectedCode, statusCode)
}
}
func TestServer_CreateSession_webauthn(t *testing.T) {
// create new session with user and request the webauthn challenge
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
// update the session with webauthn assertion data
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
}
func TestServer_CreateSession_successfulIntent(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
}
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
}
func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
// successful intent without known / linked user
idpUserID := "id"
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID)
// link the user (with info from intent)
Tester.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
// session with intent check must now succeed
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
}
func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID := Tester.CreateIntent(t, CTX, idpID)
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: "false",
},
},
})
require.Error(t, err)
}
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := Tester.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID,
})
require.NoError(t, err)
secret = resp.GetSecret()
code, err := totp.GenerateCode(secret, time.Now())
require.NoError(t, err)
_, err = Tester.Client.UserV2.VerifyTOTPRegistration(ctx, &user.VerifyTOTPRegistrationRequest{
UserId: userID,
Code: code,
})
require.NoError(t, err)
return secret
}
func registerOTPSMS(ctx context.Context, t *testing.T, userID string) {
_, err := Tester.Client.UserV2.AddOTPSMS(ctx, &user.AddOTPSMSRequest{
UserId: userID,
})
require.NoError(t, err)
}
func registerOTPEmail(ctx context.Context, t *testing.T, userID string) {
_, err := Tester.Client.UserV2.AddOTPEmail(ctx, &user.AddOTPEmailRequest{
UserId: userID,
})
require.NoError(t, err)
}
func TestServer_SetSession_flow_totp(t *testing.T) {
userExisting := createFullUser(CTX)
// create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
sessionToken := createResp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, "")
t.Run("check user", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userExisting.GetUserId(),
},
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor)
})
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId())
sessionToken = resp.GetSessionToken()
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
})
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
Tester.RegisterUserU2F(userAuthCtx, userExisting.GetUserId())
totpSecret := registerTOTP(userAuthCtx, t, userExisting.GetUserId())
registerOTPSMS(userAuthCtx, t, userExisting.GetUserId())
registerOTPEmail(userAuthCtx, t, userExisting.GetUserId())
t.Run("check TOTP", func(t *testing.T) {
code, err := totp.GenerateCode(totpSecret, time.Now())
require.NoError(t, err)
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
Totp: &session.CheckTOTP{
Code: code,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor, wantTOTPFactor)
})
userImport := Tester.CreateHumanUserWithTOTP(CTX, totpSecret)
createRespImport, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
sessionTokenImport := createRespImport.GetSessionToken()
verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, createRespImport.GetDetails().GetSequence(), time.Minute, nil, nil, 0, "")
t.Run("check user", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createRespImport.GetSessionId(),
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: userImport.GetUserId(),
},
},
},
})
require.NoError(t, err)
sessionTokenImport = resp.GetSessionToken()
verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userImport.GetUserId(), wantUserFactor)
})
t.Run("check TOTP", func(t *testing.T) {
code, err := totp.GenerateCode(totpSecret, time.Now())
require.NoError(t, err)
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createRespImport.GetSessionId(),
Checks: &session.Checks{
Totp: &session.CheckTOTP{
Code: code,
},
},
})
require.NoError(t, err)
sessionTokenImport = resp.GetSessionToken()
verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userImport.GetUserId(), wantUserFactor, wantTOTPFactor)
})
}
func TestServer_SetSession_flow(t *testing.T) {
// create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
sessionToken := createResp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
t.Run("check user", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor)
})
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
sessionToken = resp.GetSessionToken()
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
})
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
Tester.RegisterUserU2F(userAuthCtx, User.GetUserId())
totpSecret := registerTOTP(userAuthCtx, t, User.GetUserId())
registerOTPSMS(userAuthCtx, t, User.GetUserId())
registerOTPEmail(userAuthCtx, t, User.GetUserId())
t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) {
for _, userVerificationRequirement := range []session.UserVerificationRequirement{
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED,
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
} {
t.Run(userVerificationRequirement.String(), func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: userVerificationRequirement,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
sessionToken = resp.GetSessionToken()
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false)
require.NoError(t, err)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor)
})
}
})
t.Run("check TOTP", func(t *testing.T) {
code, err := totp.GenerateCode(totpSecret, time.Now())
require.NoError(t, err)
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
Totp: &session.CheckTOTP{
Code: code,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantTOTPFactor)
})
t.Run("check OTP SMS", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Challenges: &session.RequestChallenges{
OtpSms: &session.RequestChallenges_OTPSMS{ReturnCode: true},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpSms()
require.NotEmpty(t, otp)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
OtpSms: &session.CheckOTP{
Code: otp,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor)
})
t.Run("check OTP Email", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Challenges: &session.RequestChallenges{
OtpEmail: &session.RequestChallenges_OTPEmail{
DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpEmail()
require.NotEmpty(t, otp)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
OtpEmail: &session.CheckOTP{
Code: otp,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor)
})
}
func TestServer_SetSession_expired(t *testing.T) {
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Lifetime: durationpb.New(20 * time.Second),
})
require.NoError(t, err)
// test session token works
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Lifetime: durationpb.New(20 * time.Second),
})
require.NoError(t, err)
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Lifetime: durationpb.New(20 * time.Second),
})
require.Error(t, err)
}
func TestServer_DeleteSession_token(t *testing.T) {
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: gu.Ptr("invalid"),
})
require.Error(t, err)
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: gu.Ptr(createResp.GetSessionToken()),
})
require.NoError(t, err)
}
func TestServer_DeleteSession_own_session(t *testing.T) {
// create two users for the test and a session each to get tokens for authorization
user1 := Tester.CreateHumanUser(CTX)
Tester.SetUserPassword(CTX, user1.GetUserId(), integration.UserPassword, false)
_, token1, _, _ := Tester.CreatePasswordSession(t, CTX, user1.GetUserId(), integration.UserPassword)
user2 := Tester.CreateHumanUser(CTX)
Tester.SetUserPassword(CTX, user2.GetUserId(), integration.UserPassword, false)
_, token2, _, _ := Tester.CreatePasswordSession(t, CTX, user2.GetUserId(), integration.UserPassword)
// create a new session for the first user
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: user1.GetUserId(),
},
},
},
})
require.NoError(t, err)
// delete the new (user1) session must not be possible with user (has no permission)
_, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token2), &session.DeleteSessionRequest{
SessionId: createResp.GetSessionId(),
})
require.Error(t, err)
// delete the new (user1) session by themselves
_, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token1), &session.DeleteSessionRequest{
SessionId: createResp.GetSessionId(),
})
require.NoError(t, err)
}
func TestServer_DeleteSession_with_permission(t *testing.T) {
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
// delete the new session by ORG_OWNER
_, err = Client.DeleteSession(Tester.WithAuthorization(context.Background(), integration.OrgOwner), &session.DeleteSessionRequest{
SessionId: createResp.GetSessionId(),
})
require.NoError(t, err)
}
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
// create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
require.Error(t, err)
require.Nil(t, sessionResp)
}
func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
ctx := Tester.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
require.NotNil(t, id, webAuthN.GetVerifiedAt().AsTime())
require.True(t, webAuthN.GetUserVerified())
}
func Test_ZITADEL_API_session_not_found(t *testing.T) {
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
// test session token works
ctx := Tester.WithAuthorizationToken(context.Background(), token)
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
//terminate the session and test it does not work anymore
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: id,
SessionToken: gu.Ptr(token),
})
require.NoError(t, err)
ctx = Tester.WithAuthorizationToken(context.Background(), token)
_, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
}
func Test_ZITADEL_API_session_expired(t *testing.T) {
id, token, _, _ := Tester.CreateVerifiedWebAuthNSessionWithLifetime(t, CTX, User.GetUserId(), 20*time.Second)
// test session token works
ctx := Tester.WithAuthorizationToken(context.Background(), token)
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
require.Nil(t, sessionResp)
}

View File

@ -0,0 +1,739 @@
package session
import (
"context"
"net"
"net/http"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
)
var (
creationDate = time.Date(2023, 10, 10, 14, 15, 0, 0, time.UTC)
)
func Test_sessionsToPb(t *testing.T) {
now := time.Now()
past := now.Add(-time.Hour)
sessions := []*query.Session{
{ // no factor, with user agent and expiration
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
Metadata: map[string][]byte{"hello": []byte("world")},
UserAgent: domain.UserAgent{
FingerprintID: gu.Ptr("fingerprintID"),
Description: gu.Ptr("description"),
IP: net.IPv4(1, 2, 3, 4),
Header: http.Header{"foo": []string{"foo", "bar"}},
},
Expiration: now,
},
{ // user factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // password factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
PasswordFactor: query.SessionPasswordFactor{
PasswordCheckedAt: past,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // webAuthN factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
WebAuthNFactor: query.SessionWebAuthNFactor{
WebAuthNCheckedAt: past,
UserVerified: true,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // totp factor
ID: "999",
CreationDate: now,
ChangeDate: now,
Sequence: 123,
State: domain.SessionStateActive,
ResourceOwner: "me",
Creator: "he",
UserFactor: query.SessionUserFactor{
UserID: "345",
UserCheckedAt: past,
LoginName: "donald",
DisplayName: "donald duck",
ResourceOwner: "org1",
},
TOTPFactor: query.SessionTOTPFactor{
TOTPCheckedAt: past,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
}
want := []*session.Session{
{ // no factor, with user agent and expiration
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: nil,
Metadata: map[string][]byte{"hello": []byte("world")},
UserAgent: &session.UserAgent{
FingerprintId: gu.Ptr("fingerprintID"),
Description: gu.Ptr("description"),
Ip: gu.Ptr("1.2.3.4"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
},
},
ExpirationDate: timestamppb.New(now),
},
{ // user factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganizationId: "org1",
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // password factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganizationId: "org1",
},
Password: &session.PasswordFactor{
VerifiedAt: timestamppb.New(past),
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // webAuthN factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganizationId: "org1",
},
WebAuthN: &session.WebAuthNFactor{
VerifiedAt: timestamppb.New(past),
UserVerified: true,
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // totp factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
Sequence: 123,
Factors: &session.Factors{
User: &session.UserFactor{
VerifiedAt: timestamppb.New(past),
Id: "345",
LoginName: "donald",
DisplayName: "donald duck",
OrganizationId: "org1",
},
Totp: &session.TOTPFactor{
VerifiedAt: timestamppb.New(past),
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
}
out := sessionsToPb(sessions)
require.Len(t, out, len(want))
for i, got := range out {
if !proto.Equal(got, want[i]) {
t.Errorf("session %d got:\n%v\nwant:\n%v", i, got, want[i])
}
}
}
func Test_userAgentToPb(t *testing.T) {
type args struct {
ua domain.UserAgent
}
tests := []struct {
name string
args args
want *session.UserAgent
}{
{
name: "empty",
args: args{domain.UserAgent{}},
},
{
name: "fingerprint id and description",
args: args{domain.UserAgent{
FingerprintID: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
}},
want: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
},
},
{
name: "with ip",
args: args{domain.UserAgent{
FingerprintID: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
IP: net.IPv4(1, 2, 3, 4),
}},
want: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
Ip: gu.Ptr("1.2.3.4"),
},
},
{
name: "with header",
args: args{domain.UserAgent{
FingerprintID: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
Header: http.Header{
"foo": []string{"foo", "bar"},
"hello": []string{"world"},
},
}},
want: &session.UserAgent{
FingerprintId: gu.Ptr("fingerPrintID"),
Description: gu.Ptr("description"),
Header: map[string]*session.UserAgent_HeaderValues{
"foo": {Values: []string{"foo", "bar"}},
"hello": {Values: []string{"world"}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := userAgentToPb(tt.args.ua)
assert.Equal(t, tt.want, got)
})
}
}
func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery {
q, err := query.NewTextQuery(column, value, compare)
require.NoError(t, err)
return q
}
func mustNewListQuery(t testing.TB, column query.Column, list []any, compare query.ListComparison) query.SearchQuery {
q, err := query.NewListQuery(query.SessionColumnID, list, compare)
require.NoError(t, err)
return q
}
func mustNewTimestampQuery(t testing.TB, column query.Column, ts time.Time, compare query.TimestampComparison) query.SearchQuery {
q, err := query.NewTimestampQuery(column, ts, compare)
require.NoError(t, err)
return q
}
func Test_listSessionsRequestToQuery(t *testing.T) {
type args struct {
ctx context.Context
req *session.ListSessionsRequest
}
tests := []struct {
name string
args args
want *query.SessionsSearchQueries
wantErr error
}{
{
name: "default request",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 0,
Asc: false,
},
Queries: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "default request with sorting column",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
SortingColumn: session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE,
},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 0,
SortingColumn: query.SessionColumnCreationDate,
Asc: false,
},
Queries: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "with list query and sessions",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
Query: &object.ListQuery{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"4", "5", "6"},
},
}},
{Query: &session.SearchQuery_UserIdQuery{
UserIdQuery: &session.UserIDQuery{
Id: "10",
},
}},
{Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER,
},
}},
},
},
},
want: &query.SessionsSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []query.SearchQuery{
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampGreater),
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
},
{
name: "invalid argument error",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
req: &session.ListSessionsRequest{
Query: &object.ListQuery{
Offset: 10,
Limit: 20,
Asc: true,
},
Queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: nil},
},
},
},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := listSessionsRequestToQuery(tt.args.ctx, tt.args.req)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_sessionQueriesToQuery(t *testing.T) {
type args struct {
ctx context.Context
queries []*session.SearchQuery
}
tests := []struct {
name string
args args
want []query.SearchQuery
wantErr error
}{
{
name: "creator only",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
},
want: []query.SearchQuery{
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
{
name: "invalid argument",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
queries: []*session.SearchQuery{
{Query: nil},
},
},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
{
name: "creator and sessions",
args: args{
ctx: authz.NewMockContext("123", "456", "789"),
queries: []*session.SearchQuery{
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
}},
{Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"4", "5", "6"},
},
}},
},
},
want: []query.SearchQuery{
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sessionQueriesToQuery(tt.args.ctx, tt.args.queries)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_sessionQueryToQuery(t *testing.T) {
type args struct {
query *session.SearchQuery
}
tests := []struct {
name string
args args
want query.SearchQuery
wantErr error
}{
{
name: "invalid argument",
args: args{&session.SearchQuery{
Query: nil,
}},
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
},
{
name: "ids query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_IdsQuery{
IdsQuery: &session.IDsQuery{
Ids: []string{"1", "2", "3"},
},
},
}},
want: mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
},
{
name: "user id query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_UserIdQuery{
UserIdQuery: &session.UserIDQuery{
Id: "10",
},
},
}},
want: mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
},
{
name: "creation date query",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS,
},
},
}},
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampLess),
},
{
name: "creation date query with default method",
args: args{&session.SearchQuery{
Query: &session.SearchQuery_CreationDateQuery{
CreationDateQuery: &session.CreationDateQuery{
CreationDate: timestamppb.New(creationDate),
},
},
}},
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampEquals),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := sessionQueryToQuery(tt.args.query)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_userCheck(t *testing.T) {
type args struct {
user *session.CheckUser
}
tests := []struct {
name string
args args
want userSearch
wantErr error
}{
{
name: "nil user",
args: args{nil},
want: nil,
},
{
name: "by user id",
args: args{&session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: "foo",
},
}},
want: userSearchByID{"foo"},
},
{
name: "by user id",
args: args{&session.CheckUser{
Search: &session.CheckUser_LoginName{
LoginName: "bar",
},
}},
want: userSearchByLoginName{"bar"},
},
{
name: "unimplemented error",
args: args{&session.CheckUser{
Search: nil,
}},
wantErr: zerrors.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := userCheck(tt.args.user)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func Test_userVerificationRequirementToDomain(t *testing.T) {
type args struct {
req session.UserVerificationRequirement
}
tests := []struct {
args args
want domain.UserVerificationRequirement
}{
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED},
want: domain.UserVerificationRequirementUnspecified,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED},
want: domain.UserVerificationRequirementRequired,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED},
want: domain.UserVerificationRequirementPreferred,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED},
want: domain.UserVerificationRequirementDiscouraged,
},
{
args: args{999},
want: domain.UserVerificationRequirementUnspecified,
},
}
for _, tt := range tests {
t.Run(tt.args.req.String(), func(t *testing.T) {
got := userVerificationRequirementToDomain(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}
func Test_userAgentToCommand(t *testing.T) {
type args struct {
userAgent *session.UserAgent
}
tests := []struct {
name string
args args
want *domain.UserAgent
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields",
args: args{&session.UserAgent{
FingerprintId: gu.Ptr("fp1"),
Ip: gu.Ptr("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: map[string]*session.UserAgent_HeaderValues{
"hello": {
Values: []string{"foo", "bar"},
},
},
}},
want: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{
"hello": []string{"foo", "bar"},
},
},
},
{
name: "invalid ip",
args: args{&session.UserAgent{
FingerprintId: gu.Ptr("fp1"),
Ip: gu.Ptr("oops"),
Description: gu.Ptr("firefox"),
Header: map[string]*session.UserAgent_HeaderValues{
"hello": {
Values: []string{"foo", "bar"},
},
},
}},
want: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: nil,
Description: gu.Ptr("firefox"),
Header: http.Header{
"hello": []string{"foo", "bar"},
},
},
},
{
name: "nil fields",
args: args{&session.UserAgent{}},
want: &domain.UserAgent{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := userAgentToCommand(tt.args.userAgent)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -10,7 +10,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
var _ settings.SettingsServiceServer = (*Server)(nil)

View File

@ -9,7 +9,7 @@ import (
"time"
"github.com/zitadel/zitadel/internal/integration"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
var (

View File

@ -10,8 +10,8 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {

View File

@ -8,7 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {

View File

@ -16,7 +16,7 @@ import (
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"}

View File

@ -11,8 +11,8 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
)
func TestServer_GetSecuritySettings(t *testing.T) {

View File

@ -0,0 +1,57 @@
package settings
import (
"context"
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/assets"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
var _ settings.SettingsServiceServer = (*Server)(nil)
type Server struct {
settings.UnimplementedSettingsServiceServer
command *command.Commands
query *query.Queries
assetsAPIDomain func(context.Context) string
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
externalSecure bool,
) *Server {
return &Server{
command: command,
query: query,
assetsAPIDomain: assets.AssetAPI(externalSecure),
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
settings.RegisterSettingsServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return settings.SettingsService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return settings.SettingsService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return settings.SettingsService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return settings.RegisterSettingsServiceHandler
}

View File

@ -0,0 +1,34 @@
//go:build integration
package settings_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
var (
CTX, AdminCTX context.Context
Tester *integration.Tester
Client settings.SettingsServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(3 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
CTX = ctx
AdminCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
Client = Tester.Client.SettingsV2beta
return m.Run()
}())
}

View File

@ -0,0 +1,161 @@
package settings
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {
current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetLoginSettingsResponse{
Settings: loginSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.OrgID,
},
}, nil
}
func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) {
current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetPasswordComplexitySettingsResponse{
Settings: passwordComplexitySettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) {
current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetPasswordExpirySettingsResponse{
Settings: passwordExpirySettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) {
current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetBrandingSettingsResponse{
Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) {
current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetDomainSettingsResponse{
Settings: domainSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) {
current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
if err != nil {
return nil, err
}
return &settings.GetLegalAndSupportSettingsResponse{
Settings: legalAndSupportSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) {
current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()))
if err != nil {
return nil, err
}
return &settings.GetLockoutSettingsResponse{
Settings: lockoutSettingsToPb(current),
Details: &object_pb.Details{
Sequence: current.Sequence,
ChangeDate: timestamppb.New(current.ChangeDate),
ResourceOwner: current.ResourceOwner,
},
}, nil
}
func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) {
links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{}, false)
if err != nil {
return nil, err
}
return &settings.GetActiveIdentityProvidersResponse{
Details: object.ToListDetails(links.SearchResponse),
IdentityProviders: identityProvidersToPb(links.Links),
}, nil
}
func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) {
instance := authz.GetInstance(ctx)
return &settings.GetGeneralSettingsResponse{
SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()),
DefaultOrgId: instance.DefaultOrganisationID(),
DefaultLanguage: instance.DefaultLanguage().String(),
}, nil
}
func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) {
policy, err := s.query.SecurityPolicy(ctx)
if err != nil {
return nil, err
}
return &settings.GetSecuritySettingsResponse{
Settings: securityPolicyToSettingsPb(policy),
}, nil
}
func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecuritySettingsRequest) (*settings.SetSecuritySettingsResponse, error) {
details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req))
if err != nil {
return nil, err
}
return &settings.SetSecuritySettingsResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}

View File

@ -0,0 +1,245 @@
package settings
import (
"time"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
multi := make([]settings.MultiFactorType, len(current.MultiFactors))
for i, typ := range current.MultiFactors {
multi[i] = multiFactorTypeToPb(typ)
}
second := make([]settings.SecondFactorType, len(current.SecondFactors))
for i, typ := range current.SecondFactors {
second[i] = secondFactorTypeToPb(typ)
}
return &settings.LoginSettings{
AllowUsernamePassword: current.AllowUsernamePassword,
AllowRegister: current.AllowRegister,
AllowExternalIdp: current.AllowExternalIDPs,
ForceMfa: current.ForceMFA,
ForceMfaLocalOnly: current.ForceMFALocalOnly,
PasskeysType: passkeysTypeToPb(current.PasswordlessType),
HidePasswordReset: current.HidePasswordReset,
IgnoreUnknownUsernames: current.IgnoreUnknownUsernames,
AllowDomainDiscovery: current.AllowDomainDiscovery,
DisableLoginWithEmail: current.DisableLoginWithEmail,
DisableLoginWithPhone: current.DisableLoginWithPhone,
DefaultRedirectUri: current.DefaultRedirectURI,
PasswordCheckLifetime: durationpb.New(time.Duration(current.PasswordCheckLifetime)),
ExternalLoginCheckLifetime: durationpb.New(time.Duration(current.ExternalLoginCheckLifetime)),
MfaInitSkipLifetime: durationpb.New(time.Duration(current.MFAInitSkipLifetime)),
SecondFactorCheckLifetime: durationpb.New(time.Duration(current.SecondFactorCheckLifetime)),
MultiFactorCheckLifetime: durationpb.New(time.Duration(current.MultiFactorCheckLifetime)),
SecondFactors: second,
MultiFactors: multi,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func isDefaultToResourceOwnerTypePb(isDefault bool) settings.ResourceOwnerType {
if isDefault {
return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE
}
return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG
}
func passkeysTypeToPb(passwordlessType domain.PasswordlessType) settings.PasskeysType {
switch passwordlessType {
case domain.PasswordlessTypeAllowed:
return settings.PasskeysType_PASSKEYS_TYPE_ALLOWED
case domain.PasswordlessTypeNotAllowed:
return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED
default:
return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED
}
}
func secondFactorTypeToPb(secondFactorType domain.SecondFactorType) settings.SecondFactorType {
switch secondFactorType {
case domain.SecondFactorTypeTOTP:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP
case domain.SecondFactorTypeU2F:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F
case domain.SecondFactorTypeOTPEmail:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL
case domain.SecondFactorTypeOTPSMS:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS
case domain.SecondFactorTypeUnspecified:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
default:
return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
}
}
func multiFactorTypeToPb(typ domain.MultiFactorType) settings.MultiFactorType {
switch typ {
case domain.MultiFactorTypeU2FWithPIN:
return settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION
case domain.MultiFactorTypeUnspecified:
return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED
default:
return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED
}
}
func passwordComplexitySettingsToPb(current *query.PasswordComplexityPolicy) *settings.PasswordComplexitySettings {
return &settings.PasswordComplexitySettings{
MinLength: current.MinLength,
RequiresUppercase: current.HasUppercase,
RequiresLowercase: current.HasLowercase,
RequiresNumber: current.HasNumber,
RequiresSymbol: current.HasSymbol,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func passwordExpirySettingsToPb(current *query.PasswordAgePolicy) *settings.PasswordExpirySettings {
return &settings.PasswordExpirySettings{
MaxAgeDays: current.MaxAgeDays,
ExpireWarnDays: current.ExpireWarnDays,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func brandingSettingsToPb(current *query.LabelPolicy, assetPrefix string) *settings.BrandingSettings {
return &settings.BrandingSettings{
LightTheme: themeToPb(current.Light, assetPrefix, current.ResourceOwner),
DarkTheme: themeToPb(current.Dark, assetPrefix, current.ResourceOwner),
FontUrl: domain.AssetURL(assetPrefix, current.ResourceOwner, current.FontURL),
DisableWatermark: current.WatermarkDisabled,
HideLoginNameSuffix: current.HideLoginNameSuffix,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
ThemeMode: themeModeToPb(current.ThemeMode),
}
}
func themeModeToPb(themeMode domain.LabelPolicyThemeMode) settings.ThemeMode {
switch themeMode {
case domain.LabelPolicyThemeAuto:
return settings.ThemeMode_THEME_MODE_AUTO
case domain.LabelPolicyThemeLight:
return settings.ThemeMode_THEME_MODE_LIGHT
case domain.LabelPolicyThemeDark:
return settings.ThemeMode_THEME_MODE_DARK
default:
return settings.ThemeMode_THEME_MODE_AUTO
}
}
func themeToPb(theme query.Theme, assetPrefix, resourceOwner string) *settings.Theme {
return &settings.Theme{
PrimaryColor: theme.PrimaryColor,
BackgroundColor: theme.BackgroundColor,
FontColor: theme.FontColor,
WarnColor: theme.WarnColor,
LogoUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.LogoURL),
IconUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.IconURL),
}
}
func domainSettingsToPb(current *query.DomainPolicy) *settings.DomainSettings {
return &settings.DomainSettings{
LoginNameIncludesDomain: current.UserLoginMustBeDomain,
RequireOrgDomainVerification: current.ValidateOrgDomains,
SmtpSenderAddressMatchesInstanceDomain: current.SMTPSenderAddressMatchesInstanceDomain,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func legalAndSupportSettingsToPb(current *query.PrivacyPolicy) *settings.LegalAndSupportSettings {
return &settings.LegalAndSupportSettings{
TosLink: current.TOSLink,
PrivacyPolicyLink: current.PrivacyLink,
HelpLink: current.HelpLink,
SupportEmail: string(current.SupportEmail),
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
DocsLink: current.DocsLink,
CustomLink: current.CustomLink,
CustomLinkText: current.CustomLinkText,
}
}
func lockoutSettingsToPb(current *query.LockoutPolicy) *settings.LockoutSettings {
return &settings.LockoutSettings{
MaxPasswordAttempts: current.MaxPasswordAttempts,
MaxOtpAttempts: current.MaxOTPAttempts,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
}
}
func identityProvidersToPb(idps []*query.IDPLoginPolicyLink) []*settings.IdentityProvider {
providers := make([]*settings.IdentityProvider, len(idps))
for i, idp := range idps {
providers[i] = identityProviderToPb(idp)
}
return providers
}
func identityProviderToPb(idp *query.IDPLoginPolicyLink) *settings.IdentityProvider {
return &settings.IdentityProvider{
Id: idp.IDPID,
Name: domain.IDPName(idp.IDPName, idp.IDPType),
Type: idpTypeToPb(idp.IDPType),
}
}
func idpTypeToPb(idpType domain.IDPType) settings.IdentityProviderType {
switch idpType {
case domain.IDPTypeUnspecified:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
case domain.IDPTypeOIDC:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC
case domain.IDPTypeJWT:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT
case domain.IDPTypeOAuth:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH
case domain.IDPTypeLDAP:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP
case domain.IDPTypeAzureAD:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD
case domain.IDPTypeGitHub:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB
case domain.IDPTypeGitHubEnterprise:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES
case domain.IDPTypeGitLab:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB
case domain.IDPTypeGitLabSelfHosted:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED
case domain.IDPTypeGoogle:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE
case domain.IDPTypeSAML:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML
case domain.IDPTypeApple:
// Handle all remaining cases so the linter succeeds
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
default:
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
}
}
func securityPolicyToSettingsPb(policy *query.SecurityPolicy) *settings.SecuritySettings {
return &settings.SecuritySettings{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: policy.EnableIframeEmbedding,
AllowedOrigins: policy.AllowedOrigins,
},
EnableImpersonation: policy.EnableImpersonation,
}
}
func securitySettingsToCommand(req *settings.SetSecuritySettingsRequest) *command.SecurityPolicy {
return &command.SecurityPolicy{
EnableIframeEmbedding: req.GetEmbeddedIframe().GetEnabled(),
AllowedOrigins: req.GetEmbeddedIframe().GetAllowedOrigins(),
EnableImpersonation: req.GetEnableImpersonation(),
}
}

View File

@ -0,0 +1,517 @@
package settings
import (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"}
func Test_loginSettingsToPb(t *testing.T) {
arg := &query.LoginPolicy{
AllowUsernamePassword: true,
AllowRegister: true,
AllowExternalIDPs: true,
ForceMFA: true,
ForceMFALocalOnly: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
DisableLoginWithEmail: true,
DisableLoginWithPhone: true,
DefaultRedirectURI: "example.com",
PasswordCheckLifetime: database.Duration(time.Hour),
ExternalLoginCheckLifetime: database.Duration(time.Minute),
MFAInitSkipLifetime: database.Duration(time.Millisecond),
SecondFactorCheckLifetime: database.Duration(time.Microsecond),
MultiFactorCheckLifetime: database.Duration(time.Nanosecond),
SecondFactors: []domain.SecondFactorType{
domain.SecondFactorTypeTOTP,
domain.SecondFactorTypeU2F,
domain.SecondFactorTypeOTPEmail,
domain.SecondFactorTypeOTPSMS,
},
MultiFactors: []domain.MultiFactorType{
domain.MultiFactorTypeU2FWithPIN,
},
IsDefault: true,
}
want := &settings.LoginSettings{
AllowUsernamePassword: true,
AllowRegister: true,
AllowExternalIdp: true,
ForceMfa: true,
ForceMfaLocalOnly: true,
PasskeysType: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
HidePasswordReset: true,
IgnoreUnknownUsernames: true,
AllowDomainDiscovery: true,
DisableLoginWithEmail: true,
DisableLoginWithPhone: true,
DefaultRedirectUri: "example.com",
PasswordCheckLifetime: durationpb.New(time.Hour),
ExternalLoginCheckLifetime: durationpb.New(time.Minute),
MfaInitSkipLifetime: durationpb.New(time.Millisecond),
SecondFactorCheckLifetime: durationpb.New(time.Microsecond),
MultiFactorCheckLifetime: durationpb.New(time.Nanosecond),
SecondFactors: []settings.SecondFactorType{
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL,
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS,
},
MultiFactors: []settings.MultiFactorType{
settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION,
},
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := loginSettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("loginSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_isDefaultToResourceOwnerTypePb(t *testing.T) {
type args struct {
isDefault bool
}
tests := []struct {
args args
want settings.ResourceOwnerType
}{
{
args: args{false},
want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG,
},
{
args: args{true},
want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := isDefaultToResourceOwnerTypePb(tt.args.isDefault)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passkeysTypeToPb(t *testing.T) {
type args struct {
passwordlessType domain.PasswordlessType
}
tests := []struct {
args args
want settings.PasskeysType
}{
{
args: args{domain.PasswordlessTypeNotAllowed},
want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED,
},
{
args: args{domain.PasswordlessTypeAllowed},
want: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
},
{
args: args{99},
want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := passkeysTypeToPb(tt.args.passwordlessType)
assert.Equal(t, tt.want, got)
})
}
}
func Test_secondFactorTypeToPb(t *testing.T) {
type args struct {
secondFactorType domain.SecondFactorType
}
tests := []struct {
args args
want settings.SecondFactorType
}{
{
args: args{domain.SecondFactorTypeTOTP},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
},
{
args: args{domain.SecondFactorTypeU2F},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
},
{
args: args{domain.SecondFactorTypeOTPSMS},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS,
},
{
args: args{domain.SecondFactorTypeOTPEmail},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL,
},
{
args: args{domain.SecondFactorTypeUnspecified},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED,
},
{
args: args{99},
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := secondFactorTypeToPb(tt.args.secondFactorType)
assert.Equal(t, tt.want, got)
})
}
}
func Test_multiFactorTypeToPb(t *testing.T) {
type args struct {
typ domain.MultiFactorType
}
tests := []struct {
args args
want settings.MultiFactorType
}{
{
args: args{domain.MultiFactorTypeU2FWithPIN},
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION,
},
{
args: args{domain.MultiFactorTypeUnspecified},
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED,
},
{
args: args{99},
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
got := multiFactorTypeToPb(tt.args.typ)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passwordComplexitySettingsToPb(t *testing.T) {
arg := &query.PasswordComplexityPolicy{
MinLength: 12,
HasUppercase: true,
HasLowercase: true,
HasNumber: true,
HasSymbol: true,
IsDefault: true,
}
want := &settings.PasswordComplexitySettings{
MinLength: 12,
RequiresUppercase: true,
RequiresLowercase: true,
RequiresNumber: true,
RequiresSymbol: true,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := passwordComplexitySettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("passwordComplexitySettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_passwordExpirySettingsToPb(t *testing.T) {
arg := &query.PasswordAgePolicy{
ExpireWarnDays: 80,
MaxAgeDays: 90,
IsDefault: true,
}
want := &settings.PasswordExpirySettings{
ExpireWarnDays: 80,
MaxAgeDays: 90,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := passwordExpirySettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("passwordExpirySettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_brandingSettingsToPb(t *testing.T) {
arg := &query.LabelPolicy{
Light: query.Theme{
PrimaryColor: "red",
WarnColor: "white",
BackgroundColor: "blue",
FontColor: "orange",
LogoURL: "light-logo",
IconURL: "light-icon",
},
Dark: query.Theme{
PrimaryColor: "magenta",
WarnColor: "pink",
BackgroundColor: "black",
FontColor: "white",
LogoURL: "dark-logo",
IconURL: "dark-icon",
},
ResourceOwner: "me",
FontURL: "fonts",
WatermarkDisabled: true,
HideLoginNameSuffix: true,
ThemeMode: domain.LabelPolicyThemeDark,
IsDefault: true,
}
want := &settings.BrandingSettings{
LightTheme: &settings.Theme{
PrimaryColor: "red",
WarnColor: "white",
BackgroundColor: "blue",
FontColor: "orange",
LogoUrl: "http://example.com/me/light-logo",
IconUrl: "http://example.com/me/light-icon",
},
DarkTheme: &settings.Theme{
PrimaryColor: "magenta",
WarnColor: "pink",
BackgroundColor: "black",
FontColor: "white",
LogoUrl: "http://example.com/me/dark-logo",
IconUrl: "http://example.com/me/dark-icon",
},
FontUrl: "http://example.com/me/fonts",
DisableWatermark: true,
HideLoginNameSuffix: true,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
ThemeMode: settings.ThemeMode_THEME_MODE_DARK,
}
got := brandingSettingsToPb(arg, "http://example.com")
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("brandingSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_domainSettingsToPb(t *testing.T) {
arg := &query.DomainPolicy{
UserLoginMustBeDomain: true,
ValidateOrgDomains: true,
SMTPSenderAddressMatchesInstanceDomain: true,
IsDefault: true,
}
want := &settings.DomainSettings{
LoginNameIncludesDomain: true,
RequireOrgDomainVerification: true,
SmtpSenderAddressMatchesInstanceDomain: true,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := domainSettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("domainSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_legalSettingsToPb(t *testing.T) {
arg := &query.PrivacyPolicy{
TOSLink: "http://example.com/tos",
PrivacyLink: "http://example.com/pricacy",
HelpLink: "http://example.com/help",
SupportEmail: "support@zitadel.com",
IsDefault: true,
DocsLink: "http://example.com/docs",
CustomLink: "http://example.com/custom",
CustomLinkText: "Custom",
}
want := &settings.LegalAndSupportSettings{
TosLink: "http://example.com/tos",
PrivacyPolicyLink: "http://example.com/pricacy",
HelpLink: "http://example.com/help",
SupportEmail: "support@zitadel.com",
DocsLink: "http://example.com/docs",
CustomLink: "http://example.com/custom",
CustomLinkText: "Custom",
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := legalAndSupportSettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("legalSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_lockoutSettingsToPb(t *testing.T) {
arg := &query.LockoutPolicy{
MaxPasswordAttempts: 22,
MaxOTPAttempts: 22,
IsDefault: true,
}
want := &settings.LockoutSettings{
MaxPasswordAttempts: 22,
MaxOtpAttempts: 22,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := lockoutSettingsToPb(arg)
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("lockoutSettingsToPb() =\n%v\nwant\n%v", got, want)
}
}
func Test_identityProvidersToPb(t *testing.T) {
arg := []*query.IDPLoginPolicyLink{
{
IDPID: "1",
IDPName: "foo",
IDPType: domain.IDPTypeOIDC,
},
{
IDPID: "2",
IDPName: "bar",
IDPType: domain.IDPTypeGitHub,
},
}
want := []*settings.IdentityProvider{
{
Id: "1",
Name: "foo",
Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC,
},
{
Id: "2",
Name: "bar",
Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB,
},
}
got := identityProvidersToPb(arg)
require.Len(t, got, len(got))
for i, v := range got {
grpc.AllFieldsSet(t, v.ProtoReflect(), ignoreTypes...)
if !proto.Equal(v, want[i]) {
t.Errorf("identityProvidersToPb() =\n%v\nwant\n%v", got, want)
}
}
}
func Test_idpTypeToPb(t *testing.T) {
type args struct {
idpType domain.IDPType
}
tests := []struct {
args args
want settings.IdentityProviderType
}{
{
args: args{domain.IDPTypeUnspecified},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED,
},
{
args: args{domain.IDPTypeOIDC},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC,
},
{
args: args{domain.IDPTypeJWT},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT,
},
{
args: args{domain.IDPTypeOAuth},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH,
},
{
args: args{domain.IDPTypeLDAP},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP,
},
{
args: args{domain.IDPTypeAzureAD},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD,
},
{
args: args{domain.IDPTypeGitHub},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB,
},
{
args: args{domain.IDPTypeGitHubEnterprise},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES,
},
{
args: args{domain.IDPTypeGitLab},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB,
},
{
args: args{domain.IDPTypeGitLabSelfHosted},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED,
},
{
args: args{domain.IDPTypeGoogle},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE,
},
{
args: args{domain.IDPTypeSAML},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML,
},
{
args: args{99},
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.want.String(), func(t *testing.T) {
if got := idpTypeToPb(tt.args.idpType); !reflect.DeepEqual(got, tt.want) {
t.Errorf("idpTypeToPb() = %v, want %v", got, tt.want)
}
})
}
}
func Test_securityPolicyToSettingsPb(t *testing.T) {
want := &settings.SecuritySettings{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
}
got := securityPolicyToSettingsPb(&query.SecurityPolicy{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo", "bar"},
EnableImpersonation: true,
})
assert.Equal(t, want, got)
}
func Test_securitySettingsToCommand(t *testing.T) {
want := &command.SecurityPolicy{
EnableIframeEmbedding: true,
AllowedOrigins: []string{"foo", "bar"},
EnableImpersonation: true,
}
got := securitySettingsToCommand(&settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
})
assert.Equal(t, want, got)
}

View File

@ -0,0 +1,174 @@
//go:build integration
package settings_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
)
func TestServer_GetSecuritySettings(t *testing.T) {
_, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
})
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
want *settings.GetSecuritySettingsResponse
wantErr bool
}{
{
name: "permission error",
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
wantErr: true,
},
{
name: "success",
ctx: AdminCTX,
want: &settings.GetSecuritySettingsResponse{
Settings: &settings.SecuritySettings{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo", "bar"},
},
EnableImpersonation: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{})
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
got, want := resp.GetSettings(), tt.want.GetSettings()
assert.Equal(t, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding")
assert.Equal(t, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins")
assert.Equal(t, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation")
})
}
}
func TestServer_SetSecuritySettings(t *testing.T) {
type args struct {
ctx context.Context
req *settings.SetSecuritySettingsRequest
}
tests := []struct {
name string
args args
want *settings.SetSecuritySettingsResponse
wantErr bool
}{
{
name: "permission error",
args: args{
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
},
EnableImpersonation: true,
},
},
wantErr: true,
},
{
name: "success allowed origins",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
AllowedOrigins: []string{"foo.com", "bar.com"},
},
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success enable iframe embedding",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
},
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success impersonation",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EnableImpersonation: true,
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "success all",
args: args{
ctx: AdminCTX,
req: &settings.SetSecuritySettingsRequest{
EmbeddedIframe: &settings.EmbeddedIframeSettings{
Enabled: true,
AllowedOrigins: []string{"foo.com", "bar.com"},
},
EnableImpersonation: true,
},
},
want: &settings.SetSecuritySettingsResponse{
Details: &object_pb.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.SetSecuritySettings(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -5,7 +5,6 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
)
@ -255,22 +254,6 @@ func UserAuthMethodToWebAuthNTokenPb(token *query.AuthMethod) *user_pb.WebAuthNT
}
}
func ExternalIDPViewsToExternalIDPs(externalIDPs []*query.IDPUserLink) []*domain.UserIDPLink {
idps := make([]*domain.UserIDPLink, len(externalIDPs))
for i, idp := range externalIDPs {
idps[i] = &domain.UserIDPLink{
ObjectRoot: models.ObjectRoot{
AggregateID: idp.UserID,
ResourceOwner: idp.ResourceOwner,
},
IDPConfigID: idp.IDPID,
ExternalUserID: idp.ProvidedUserID,
DisplayName: idp.ProvidedUsername,
}
}
return idps
}
func TypeToPb(userType domain.UserType) user_pb.Type {
switch userType {
case domain.UserTypeHuman:

View File

@ -17,8 +17,8 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/integration"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
)

View File

@ -7,8 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {

View File

@ -12,9 +12,10 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_SetEmail(t *testing.T) {

View File

@ -0,0 +1,94 @@
package user
import (
"context"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{
IDPID: req.GetIdpLink().GetIdpId(),
DisplayName: req.GetIdpLink().GetUserName(),
IDPExternalID: req.GetIdpLink().GetUserId(),
})
if err != nil {
return nil, err
}
return &user.AddIDPLinkResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest) (_ *user.ListIDPLinksResponse, err error) {
queries, err := ListLinkedIDPsRequestToQuery(req)
if err != nil {
return nil, err
}
res, err := s.query.IDPUserLinks(ctx, queries, false)
if err != nil {
return nil, err
}
res.RemoveNoPermission(ctx, s.checkPermission)
return &user.ListIDPLinksResponse{
Result: IDPLinksToPb(res.Links),
Details: object.ToListDetails(res.SearchResponse),
}, nil
}
func ListLinkedIDPsRequestToQuery(req *user.ListIDPLinksRequest) (*query.IDPUserLinksSearchQuery, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
userQuery, err := query.NewIDPUserLinksUserIDSearchQuery(req.UserId)
if err != nil {
return nil, err
}
return &query.IDPUserLinksSearchQuery{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: []query.SearchQuery{userQuery},
}, nil
}
func IDPLinksToPb(res []*query.IDPUserLink) []*user.IDPLink {
links := make([]*user.IDPLink, len(res))
for i, link := range res {
links[i] = IDPLinkToPb(link)
}
return links
}
func IDPLinkToPb(link *query.IDPUserLink) *user.IDPLink {
return &user.IDPLink{
IdpId: link.IDPID,
UserId: link.ProvidedUserID,
UserName: link.ProvidedUsername,
}
}
func (s *Server) RemoveIDPLink(ctx context.Context, req *user.RemoveIDPLinkRequest) (*user.RemoveIDPLinkResponse, error) {
objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveIDPLinkRequestToDomain(ctx, req))
if err != nil {
return nil, err
}
return &user.RemoveIDPLinkResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}
func RemoveIDPLinkRequestToDomain(ctx context.Context, req *user.RemoveIDPLinkRequest) *domain.UserIDPLink {
return &domain.UserIDPLink{
ObjectRoot: models.ObjectRoot{
AggregateID: req.UserId,
},
IDPConfigID: req.IdpId,
ExternalUserID: req.LinkedUserId,
}
}

View File

@ -0,0 +1,360 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AddIDPLink(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
type args struct {
ctx context.Context
req *user.AddIDPLinkRequest
}
tests := []struct {
name string
args args
want *user.AddIDPLinkResponse
wantErr bool
}{
{
name: "user does not exist",
args: args{
CTX,
&user.AddIDPLinkRequest{
UserId: "userID",
IdpLink: &user.IDPLink{
IdpId: idpID,
UserId: "userID",
UserName: "username",
},
},
},
want: nil,
wantErr: true,
},
{
name: "idp does not exist",
args: args{
CTX,
&user.AddIDPLinkRequest{
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
IdpLink: &user.IDPLink{
IdpId: "idpID",
UserId: "userID",
UserName: "username",
},
},
},
want: nil,
wantErr: true,
},
{
name: "add link",
args: args{
CTX,
&user.AddIDPLinkRequest{
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
IdpLink: &user.IDPLink{
IdpId: idpID,
UserId: "userID",
UserName: "username",
},
},
},
want: &user.AddIDPLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_ListIDPLinks(t *testing.T) {
orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX)
userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
Tester.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpID, "externalUsername_instance")
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, IamCTX, orgResp.OrganizationId)
userOrgResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org")
userMultipleResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
Tester.CreateUserIDPlink(IamCTX, userMultipleResp.GetUserId(), "external_multi", instanceIdpID, "externalUsername_multi")
Tester.CreateUserIDPlink(IamCTX, userMultipleResp.GetUserId(), "external_multi", orgIdpID, "externalUsername_multi")
type args struct {
ctx context.Context
req *user.ListIDPLinksRequest
}
tests := []struct {
name string
args args
want *user.ListIDPLinksResponse
wantErr bool
}{
{
name: "list links, no permission",
args: args{
UserCTX,
&user.ListIDPLinksRequest{
UserId: userOrgResp.GetUserId(),
},
},
want: &user.ListIDPLinksResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Result: []*user.IDPLink{},
},
},
{
name: "list links, no permission, org",
args: args{
CTX,
&user.ListIDPLinksRequest{
UserId: userOrgResp.GetUserId(),
},
},
want: &user.ListIDPLinksResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Result: []*user.IDPLink{},
},
},
{
name: "list idp links, org, ok",
args: args{
IamCTX,
&user.ListIDPLinksRequest{
UserId: userOrgResp.GetUserId(),
},
},
want: &user.ListIDPLinksResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Result: []*user.IDPLink{
{
IdpId: orgIdpID,
UserId: "external_org",
UserName: "externalUsername_org",
},
},
},
},
{
name: "list idp links, instance, ok",
args: args{
IamCTX,
&user.ListIDPLinksRequest{
UserId: userInstanceResp.GetUserId(),
},
},
want: &user.ListIDPLinksResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Result: []*user.IDPLink{
{
IdpId: instanceIdpID,
UserId: "external_instance",
UserName: "externalUsername_instance",
},
},
},
},
{
name: "list idp links, multi, ok",
args: args{
IamCTX,
&user.ListIDPLinksRequest{
UserId: userMultipleResp.GetUserId(),
},
},
want: &user.ListIDPLinksResponse{
Details: &object.ListDetails{
TotalResult: 2,
Timestamp: timestamppb.Now(),
},
Result: []*user.IDPLink{
{
IdpId: instanceIdpID,
UserId: "external_multi",
UserName: "externalUsername_multi",
},
{
IdpId: orgIdpID,
UserId: "external_multi",
UserName: "externalUsername_multi",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
retryDuration := time.Minute
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Client.ListIDPLinks(tt.args.ctx, tt.args.req)
assertErr := assert.NoError
if tt.wantErr {
assertErr = assert.Error
}
assertErr(ttt, listErr)
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected idplinks result")
})
}
}
func TestServer_RemoveIDPLink(t *testing.T) {
orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX)
userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
Tester.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpID, "externalUsername_instance")
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, IamCTX, orgResp.OrganizationId)
userOrgResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org")
userNoLinkResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
type args struct {
ctx context.Context
req *user.RemoveIDPLinkRequest
}
tests := []struct {
name string
args args
want *user.RemoveIDPLinkResponse
wantErr bool
}{
{
name: "remove link, no permission",
args: args{
UserCTX,
&user.RemoveIDPLinkRequest{
UserId: userOrgResp.GetUserId(),
IdpId: orgIdpID,
LinkedUserId: "external_org",
},
},
wantErr: true,
},
{
name: "remove link, no permission, org",
args: args{
CTX,
&user.RemoveIDPLinkRequest{
UserId: userOrgResp.GetUserId(),
IdpId: orgIdpID,
LinkedUserId: "external_org",
},
},
wantErr: true,
},
{
name: "remove link, org, ok",
args: args{
IamCTX,
&user.RemoveIDPLinkRequest{
UserId: userOrgResp.GetUserId(),
IdpId: orgIdpID,
LinkedUserId: "external_org",
},
},
want: &user.RemoveIDPLinkResponse{
Details: &object.Details{
ResourceOwner: orgResp.GetOrganizationId(),
ChangeDate: timestamppb.Now(),
},
},
},
{
name: "remove link, instance, ok",
args: args{
IamCTX,
&user.RemoveIDPLinkRequest{
UserId: userInstanceResp.GetUserId(),
IdpId: instanceIdpID,
LinkedUserId: "external_instance",
},
},
want: &user.RemoveIDPLinkResponse{
Details: &object.Details{
ResourceOwner: orgResp.GetOrganizationId(),
ChangeDate: timestamppb.Now(),
},
},
},
{
name: "remove link, no link, error",
args: args{
IamCTX,
&user.RemoveIDPLinkRequest{
UserId: userNoLinkResp.GetUserId(),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveIDPLink(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -4,7 +4,7 @@ import (
"context"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {

View File

@ -9,9 +9,10 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_AddOTPSMS(t *testing.T) {

View File

@ -7,9 +7,10 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
@ -116,3 +117,68 @@ func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*use
},
}, nil
}
func (s *Server) RemovePasskey(ctx context.Context, req *user.RemovePasskeyRequest) (*user.RemovePasskeyResponse, error) {
objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.GetUserId(), req.GetPasskeyId(), "")
if err != nil {
return nil, err
}
return &user.RemovePasskeyResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}
func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest) (*user.ListPasskeysResponse, error) {
query := new(query.UserAuthMethodSearchQueries)
err := query.AppendUserIDQuery(req.UserId)
if err != nil {
return nil, err
}
err = query.AppendAuthMethodQuery(domain.UserAuthMethodTypePasswordless)
if err != nil {
return nil, err
}
err = query.AppendStateQuery(domain.MFAStateReady)
if err != nil {
return nil, err
}
authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false)
authMethods.RemoveNoPermission(ctx, s.checkPermission)
if err != nil {
return nil, err
}
return &user.ListPasskeysResponse{
Details: object.ToListDetails(authMethods.SearchResponse),
Result: authMethodsToPasskeyPb(authMethods),
}, nil
}
func authMethodsToPasskeyPb(methods *query.AuthMethods) []*user.Passkey {
t := make([]*user.Passkey, len(methods.AuthMethods))
for i, token := range methods.AuthMethods {
t[i] = authMethodToPasskeyPb(token)
}
return t
}
func authMethodToPasskeyPb(token *query.AuthMethod) *user.Passkey {
return &user.Passkey{
Id: token.TokenID,
State: mfaStateToPb(token.State),
Name: token.Name,
}
}
func mfaStateToPb(state domain.MFAState) user.AuthFactorState {
switch state {
case domain.MFAStateNotReady:
return user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY
case domain.MFAStateReady:
return user.AuthFactorState_AUTH_FACTOR_STATE_READY
case domain.MFAStateUnspecified, domain.MFAStateRemoved:
// Handle all remaining cases so the linter succeeds
return user.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED
default:
return user.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED
}
}

View File

@ -5,6 +5,7 @@ package user_test
import (
"context"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
@ -12,9 +13,10 @@ import (
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_RegisterPasskey(t *testing.T) {
@ -138,19 +140,7 @@ func TestServer_RegisterPasskey(t *testing.T) {
}
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
require.NoError(t, err)
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
})
require.NoError(t, err)
require.NotEmpty(t, pkr.GetPasskeyId())
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
userID, pkr := userWithPasskeyRegistered(t)
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
@ -317,3 +307,291 @@ func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
})
}
}
func userWithPasskeyRegistered(t *testing.T) (string, *user.RegisterPasskeyResponse) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
return userID, passkeyRegister(t, userID)
}
func userWithPasskeyVerified(t *testing.T) (string, string) {
userID, pkr := userWithPasskeyRegistered(t)
return userID, passkeyVerify(t, userID, pkr)
}
func passkeyRegister(t *testing.T, userID string) *user.RegisterPasskeyResponse {
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
require.NoError(t, err)
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
})
require.NoError(t, err)
require.NotEmpty(t, pkr.GetPasskeyId())
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
return pkr
}
func passkeyVerify(t *testing.T, userID string, pkr *user.RegisterPasskeyResponse) string {
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
_, err = Client.VerifyPasskeyRegistration(CTX, &user.VerifyPasskeyRegistrationRequest{
UserId: userID,
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: attestationResponse,
PasskeyName: "nice name",
})
require.NoError(t, err)
return pkr.GetPasskeyId()
}
func TestServer_RemovePasskey(t *testing.T) {
userIDWithout := Tester.CreateHumanUser(CTX).GetUserId()
userIDRegistered, pkrRegistered := userWithPasskeyRegistered(t)
userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t)
userIDVerifiedPermission, passkeyIDVerifiedPermission := userWithPasskeyVerified(t)
type args struct {
ctx context.Context
req *user.RemovePasskeyRequest
}
tests := []struct {
name string
args args
want *user.RemovePasskeyResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: IamCTX,
req: &user.RemovePasskeyRequest{
PasskeyId: "123",
},
},
wantErr: true,
},
{
name: "missing passkey id",
args: args{
ctx: IamCTX,
req: &user.RemovePasskeyRequest{
UserId: "123",
},
},
wantErr: true,
},
{
name: "success, registered",
args: args{
ctx: IamCTX,
req: &user.RemovePasskeyRequest{
UserId: userIDRegistered,
PasskeyId: pkrRegistered.GetPasskeyId(),
},
},
want: &user.RemovePasskeyResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "no passkey, error",
args: args{
ctx: IamCTX,
req: &user.RemovePasskeyRequest{
UserId: userIDWithout,
PasskeyId: pkrRegistered.GetPasskeyId(),
},
},
wantErr: true,
},
{
name: "success, verified",
args: args{
ctx: IamCTX,
req: &user.RemovePasskeyRequest{
UserId: userIDVerified,
PasskeyId: passkeyIDVerified,
},
},
want: &user.RemovePasskeyResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "verified, permission error",
args: args{
ctx: UserCTX,
req: &user.RemovePasskeyRequest{
UserId: userIDVerifiedPermission,
PasskeyId: passkeyIDVerifiedPermission,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemovePasskey(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_ListPasskeys(t *testing.T) {
userIDWithout := Tester.CreateHumanUser(CTX).GetUserId()
userIDRegistered, _ := userWithPasskeyRegistered(t)
userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t)
userIDMulti, passkeyIDMulti1 := userWithPasskeyVerified(t)
passkeyIDMulti2 := passkeyVerify(t, userIDMulti, passkeyRegister(t, userIDMulti))
type args struct {
ctx context.Context
req *user.ListPasskeysRequest
}
tests := []struct {
name string
args args
want *user.ListPasskeysResponse
wantErr bool
}{
{
name: "list passkeys, no permission",
args: args{
UserCTX,
&user.ListPasskeysRequest{
UserId: userIDVerified,
},
},
want: &user.ListPasskeysResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Result: []*user.Passkey{},
},
},
{
name: "list passkeys, none",
args: args{
UserCTX,
&user.ListPasskeysRequest{
UserId: userIDWithout,
},
},
want: &user.ListPasskeysResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Result: []*user.Passkey{},
},
},
{
name: "list passkeys, registered",
args: args{
UserCTX,
&user.ListPasskeysRequest{
UserId: userIDRegistered,
},
},
want: &user.ListPasskeysResponse{
Details: &object.ListDetails{
TotalResult: 0,
Timestamp: timestamppb.Now(),
},
Result: []*user.Passkey{},
},
},
{
name: "list passkeys, ok",
args: args{
IamCTX,
&user.ListPasskeysRequest{
UserId: userIDVerified,
},
},
want: &user.ListPasskeysResponse{
Details: &object.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
Result: []*user.Passkey{
{
Id: passkeyIDVerified,
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
Name: "nice name",
},
},
},
},
{
name: "list idp links, multi, ok",
args: args{
IamCTX,
&user.ListPasskeysRequest{
UserId: userIDMulti,
},
},
want: &user.ListPasskeysResponse{
Details: &object.ListDetails{
TotalResult: 2,
Timestamp: timestamppb.Now(),
},
Result: []*user.Passkey{
{
Id: passkeyIDMulti1,
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
Name: "nice name",
},
{
Id: passkeyIDMulti2,
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
Name: "nice name",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
retryDuration := time.Minute
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Client.ListPasskeys(tt.args.ctx, tt.args.req)
assertErr := assert.NoError
if tt.wantErr {
assertErr = assert.Error
}
assertErr(ttt, listErr)
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected idplinks result")
})
}
}

View File

@ -14,8 +14,8 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_passkeyAuthenticatorToDomain(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) {

View File

@ -11,9 +11,10 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_RequestPasswordReset(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_notificationTypeToDomain(t *testing.T) {

View File

@ -7,8 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {

View File

@ -13,9 +13,10 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_SetPhone(t *testing.T) {

View File

@ -11,7 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) {

View File

@ -13,9 +13,10 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_GetUserByID(t *testing.T) {

View File

@ -11,7 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
var _ user.UserServiceServer = (*Server)(nil)

View File

@ -5,7 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) {

View File

@ -12,9 +12,10 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_RegisterTOTP(t *testing.T) {

View File

@ -10,8 +10,8 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_totpDetailsToPb(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) {
@ -40,3 +40,13 @@ func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FR
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}
func (s *Server) RemoveU2F(ctx context.Context, req *user.RemoveU2FRequest) (*user.RemoveU2FResponse, error) {
objectDetails, err := s.command.HumanRemoveU2F(ctx, req.GetUserId(), req.GetU2FId(), "")
if err != nil {
return nil, err
}
return &user.RemoveU2FResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}

View File

@ -11,9 +11,10 @@ import (
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_RegisterU2F(t *testing.T) {
@ -106,16 +107,7 @@ func TestServer_RegisterU2F(t *testing.T) {
}
func TestServer_VerifyU2FRegistration(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{
UserId: userID,
})
require.NoError(t, err)
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t)
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
@ -188,3 +180,138 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
})
}
}
func ctxFromNewUserWithRegisteredU2F(t *testing.T) (context.Context, string, *user.RegisterU2FResponse) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{
UserId: userID,
})
require.NoError(t, err)
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
return ctx, userID, pkr
}
func ctxFromNewUserWithVerifiedU2F(t *testing.T) (context.Context, string, string) {
ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t)
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
_, err = Client.VerifyU2FRegistration(ctx, &user.VerifyU2FRegistrationRequest{
UserId: userID,
U2FId: pkr.GetU2FId(),
PublicKeyCredential: attestationResponse,
TokenName: "nice name",
})
require.NoError(t, err)
return ctx, userID, pkr.GetU2FId()
}
func TestServer_RemoveU2F(t *testing.T) {
userIDWithout := Tester.CreateHumanUser(CTX).GetUserId()
ctxRegistered, userIDRegistered, pkrRegistered := ctxFromNewUserWithRegisteredU2F(t)
_, userIDVerified, u2fVerified := ctxFromNewUserWithVerifiedU2F(t)
_, userIDVerifiedPermission, u2fVerifiedPermission := ctxFromNewUserWithVerifiedU2F(t)
type args struct {
ctx context.Context
req *user.RemoveU2FRequest
}
tests := []struct {
name string
args args
want *user.RemoveU2FResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: ctxRegistered,
req: &user.RemoveU2FRequest{
U2FId: "123",
},
},
wantErr: true,
},
{
name: "missing u2f id",
args: args{
ctx: ctxRegistered,
req: &user.RemoveU2FRequest{
UserId: "123",
},
},
wantErr: true,
},
{
name: "success, registered",
args: args{
ctx: ctxRegistered,
req: &user.RemoveU2FRequest{
UserId: userIDRegistered,
U2FId: pkrRegistered.GetU2FId(),
},
},
want: &user.RemoveU2FResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "no u2f, error",
args: args{
ctx: IamCTX,
req: &user.RemoveU2FRequest{
UserId: userIDWithout,
U2FId: pkrRegistered.GetU2FId(),
},
},
wantErr: true,
},
{
name: "success, IAMOwner permission, verified",
args: args{
ctx: IamCTX,
req: &user.RemoveU2FRequest{
UserId: userIDVerified,
U2FId: u2fVerified,
},
},
want: &user.RemoveU2FResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "verified, permission error",
args: args{
ctx: UserCTX,
req: &user.RemoveU2FRequest{
UserId: userIDVerifiedPermission,
U2FId: u2fVerifiedPermission,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveU2F(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -13,8 +13,8 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_u2fRegistrationDetailsToPb(t *testing.T) {

View File

@ -18,11 +18,12 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
human, err := AddUserRequestToAddHuman(req)
if err != nil {
return nil, err
@ -259,20 +260,6 @@ func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password {
}
}
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{
IDPID: req.GetIdpLink().GetIdpId(),
DisplayName: req.GetIdpLink().GetUserName(),
IDPExternalID: req.GetIdpLink().GetUserId(),
})
if err != nil {
return nil, err
}
return &user.AddIDPLinkResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
if err != nil {

View File

@ -18,12 +18,13 @@ import (
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/idp"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
var (
@ -1797,86 +1798,6 @@ func TestServer_DeleteUser(t *testing.T) {
}
}
func TestServer_AddIDPLink(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
type args struct {
ctx context.Context
req *user.AddIDPLinkRequest
}
tests := []struct {
name string
args args
want *user.AddIDPLinkResponse
wantErr bool
}{
{
name: "user does not exist",
args: args{
CTX,
&user.AddIDPLinkRequest{
UserId: "userID",
IdpLink: &user.IDPLink{
IdpId: idpID,
UserId: "userID",
UserName: "username",
},
},
},
want: nil,
wantErr: true,
},
{
name: "idp does not exist",
args: args{
CTX,
&user.AddIDPLinkRequest{
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
IdpLink: &user.IDPLink{
IdpId: "idpID",
UserId: "userID",
UserName: "username",
},
},
},
want: nil,
wantErr: true,
},
{
name: "add link",
args: args{
CTX,
&user.AddIDPLinkRequest{
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
IdpLink: &user.IDPLink{
IdpId: idpID,
UserId: "userID",
UserName: "username",
},
},
},
want: &user.AddIDPLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_StartIdentityProviderIntent(t *testing.T) {
idpID := Tester.AddGenericOAuthProvider(t, CTX)
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, Tester.Organisation.ID)

View File

@ -17,8 +17,8 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_idpIntentToIDPIntentPb(t *testing.T) {

View File

@ -0,0 +1,86 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
var email *domain.Email
switch v := req.GetVerification().(type) {
case *user.SetEmailRequest_SendCode:
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
case *user.SetEmailRequest_ReturnCode:
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg)
case *user.SetEmailRequest_IsVerified:
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), req.GetEmail())
case nil:
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v)
}
if err != nil {
return nil, err
}
return &user.SetEmailResponse{
Details: &object.Details{
Sequence: email.Sequence,
ChangeDate: timestamppb.New(email.ChangeDate),
ResourceOwner: email.ResourceOwner,
},
VerificationCode: email.PlainCode,
}, nil
}
func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.ResendEmailCodeResponse, err error) {
var email *domain.Email
switch v := req.GetVerification().(type) {
case *user.ResendEmailCodeRequest_SendCode:
email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
case *user.ResendEmailCodeRequest_ReturnCode:
email, err = s.command.ResendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg)
case nil:
email, err = s.command.ResendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method ResendEmailCode not implemented", v)
}
if err != nil {
return nil, err
}
return &user.ResendEmailCodeResponse{
Details: &object.Details{
Sequence: email.Sequence,
ChangeDate: timestamppb.New(email.ChangeDate),
ResourceOwner: email.ResourceOwner,
},
VerificationCode: email.PlainCode,
}, nil
}
func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) {
details, err := s.command.VerifyUserEmail(ctx,
req.GetUserId(),
req.GetVerificationCode(),
s.userCodeAlg,
)
if err != nil {
return nil, err
}
return &user.VerifyEmailResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}

View File

@ -0,0 +1,297 @@
//go:build integration
package user_test
import (
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_SetEmail(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
tests := []struct {
name string
req *user.SetEmailRequest
want *user.SetEmailResponse
wantErr bool
}{
{
name: "user not existing",
req: &user.SetEmailRequest{
UserId: "xxx",
Email: "default-verifier@mouse.com",
},
wantErr: true,
},
{
name: "default verfication",
req: &user.SetEmailRequest{
UserId: userID,
Email: "default-verifier@mouse.com",
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "custom url template",
req: &user.SetEmailRequest{
UserId: userID,
Email: "custom-url@mouse.com",
Verification: &user.SetEmailRequest_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
},
},
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "template error",
req: &user.SetEmailRequest{
UserId: userID,
Email: "custom-url@mouse.com",
Verification: &user.SetEmailRequest_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("{{"),
},
},
},
wantErr: true,
},
{
name: "return code",
req: &user.SetEmailRequest{
UserId: userID,
Email: "return-code@mouse.com",
Verification: &user.SetEmailRequest_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
VerificationCode: gu.Ptr("xxx"),
},
},
{
name: "is verified true",
req: &user.SetEmailRequest{
UserId: userID,
Email: "verified-true@mouse.com",
Verification: &user.SetEmailRequest_IsVerified{
IsVerified: true,
},
},
want: &user.SetEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "is verified false",
req: &user.SetEmailRequest{
UserId: userID,
Email: "verified-false@mouse.com",
Verification: &user.SetEmailRequest_IsVerified{
IsVerified: false,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.SetEmail(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetVerificationCode() != "" {
assert.NotEmpty(t, got.GetVerificationCode())
}
})
}
}
func TestServer_ResendEmailCode(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId()
tests := []struct {
name string
req *user.ResendEmailCodeRequest
want *user.ResendEmailCodeResponse
wantErr bool
}{
{
name: "user not existing",
req: &user.ResendEmailCodeRequest{
UserId: "xxx",
},
wantErr: true,
},
{
name: "user no code",
req: &user.ResendEmailCodeRequest{
UserId: verifiedUserID,
},
wantErr: true,
},
{
name: "resend",
req: &user.ResendEmailCodeRequest{
UserId: userID,
},
want: &user.ResendEmailCodeResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "custom url template",
req: &user.ResendEmailCodeRequest{
UserId: userID,
Verification: &user.ResendEmailCodeRequest_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
},
},
},
want: &user.ResendEmailCodeResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "template error",
req: &user.ResendEmailCodeRequest{
UserId: userID,
Verification: &user.ResendEmailCodeRequest_SendCode{
SendCode: &user.SendEmailVerificationCode{
UrlTemplate: gu.Ptr("{{"),
},
},
},
wantErr: true,
},
{
name: "return code",
req: &user.ResendEmailCodeRequest{
UserId: userID,
Verification: &user.ResendEmailCodeRequest_ReturnCode{
ReturnCode: &user.ReturnEmailVerificationCode{},
},
},
want: &user.ResendEmailCodeResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
VerificationCode: gu.Ptr("xxx"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ResendEmailCode(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetVerificationCode() != "" {
assert.NotEmpty(t, got.GetVerificationCode())
}
})
}
}
func TestServer_VerifyEmail(t *testing.T) {
userResp := Tester.CreateHumanUser(CTX)
tests := []struct {
name string
req *user.VerifyEmailRequest
want *user.VerifyEmailResponse
wantErr bool
}{
{
name: "wrong code",
req: &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: "xxx",
},
wantErr: true,
},
{
name: "wrong user",
req: &user.VerifyEmailRequest{
UserId: "xxx",
VerificationCode: userResp.GetEmailCode(),
},
wantErr: true,
},
{
name: "verify user",
req: &user.VerifyEmailRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetEmailCode(),
},
want: &user.VerifyEmailResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.VerifyEmail(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,42 @@
package user
import (
"context"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil
}
func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
}
func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) {
details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil
}
func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) {
objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
}

View File

@ -0,0 +1,362 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_AddOTPSMS(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, otherUser)
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
userVerified := Tester.CreateHumanUser(CTX)
_, err := Client.VerifyPhone(CTX, &user.VerifyPhoneRequest{
UserId: userVerified.GetUserId(),
VerificationCode: userVerified.GetPhoneCode(),
})
require.NoError(t, err)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
userVerified2 := Tester.CreateHumanUser(CTX)
_, err = Client.VerifyPhone(CTX, &user.VerifyPhoneRequest{
UserId: userVerified2.GetUserId(),
VerificationCode: userVerified2.GetPhoneCode(),
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.AddOTPSMSRequest
}
tests := []struct {
name string
args args
want *user.AddOTPSMSResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.AddOTPSMSRequest{},
},
wantErr: true,
},
{
name: "user mismatch",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser),
req: &user.AddOTPSMSRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "phone not verified",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.AddOTPSMSRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "add success",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified),
req: &user.AddOTPSMSRequest{
UserId: userVerified.GetUserId(),
},
},
want: &user.AddOTPSMSResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "add success, admin",
args: args{
ctx: CTX,
req: &user.AddOTPSMSRequest{
UserId: userVerified2.GetUserId(),
},
},
want: &user.AddOTPSMSResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddOTPSMS(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemoveOTPSMS(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
_, err := Client.VerifyPhone(userVerifiedCtx, &user.VerifyPhoneRequest{
UserId: userVerified.GetUserId(),
VerificationCode: userVerified.GetPhoneCode(),
})
require.NoError(t, err)
_, err = Client.AddOTPSMS(userVerifiedCtx, &user.AddOTPSMSRequest{UserId: userVerified.GetUserId()})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveOTPSMSRequest
}
tests := []struct {
name string
args args
want *user.RemoveOTPSMSResponse
wantErr bool
}{
{
name: "not added",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.RemoveOTPSMSRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: userVerifiedCtx,
req: &user.RemoveOTPSMSRequest{
UserId: userVerified.GetUserId(),
},
},
want: &user.RemoveOTPSMSResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ResourceOwner,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveOTPSMS(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_AddOTPEmail(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, otherUser)
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
userVerified := Tester.CreateHumanUser(CTX)
_, err := Client.VerifyEmail(CTX, &user.VerifyEmailRequest{
UserId: userVerified.GetUserId(),
VerificationCode: userVerified.GetEmailCode(),
})
require.NoError(t, err)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
userVerified2 := Tester.CreateHumanUser(CTX)
_, err = Client.VerifyEmail(CTX, &user.VerifyEmailRequest{
UserId: userVerified2.GetUserId(),
VerificationCode: userVerified2.GetEmailCode(),
})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.AddOTPEmailRequest
}
tests := []struct {
name string
args args
want *user.AddOTPEmailResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.AddOTPEmailRequest{},
},
wantErr: true,
},
{
name: "user mismatch",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser),
req: &user.AddOTPEmailRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "email not verified",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.AddOTPEmailRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "add success",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified),
req: &user.AddOTPEmailRequest{
UserId: userVerified.GetUserId(),
},
},
want: &user.AddOTPEmailResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "add success, admin",
args: args{
ctx: CTX,
req: &user.AddOTPEmailRequest{
UserId: userVerified2.GetUserId(),
},
},
want: &user.AddOTPEmailResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.AddOTPEmail(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemoveOTPEmail(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
_, err := Client.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{
UserId: userVerified.GetUserId(),
VerificationCode: userVerified.GetEmailCode(),
})
require.NoError(t, err)
_, err = Client.AddOTPEmail(userVerifiedCtx, &user.AddOTPEmailRequest{UserId: userVerified.GetUserId()})
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveOTPEmailRequest
}
tests := []struct {
name string
args args
want *user.RemoveOTPEmailResponse
wantErr bool
}{
{
name: "not added",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.RemoveOTPEmailRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: userVerifiedCtx,
req: &user.RemoveOTPEmailRequest{
UserId: userVerified.GetUserId(),
},
},
want: &user.RemoveOTPEmailResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ResourceOwner,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveOTPEmail(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,118 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/structpb"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
var (
authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator())
)
if code := req.GetCode(); code != nil {
return passkeyRegistrationDetailsToPb(
s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), "", authenticator, code.Id, code.Code, req.GetDomain(), s.userCodeAlg),
)
}
return passkeyRegistrationDetailsToPb(
s.command.RegisterUserPasskey(ctx, req.GetUserId(), "", req.GetDomain(), authenticator),
)
}
func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.AuthenticatorAttachment {
switch pa {
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED:
return domain.AuthenticatorAttachmentUnspecified
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM:
return domain.AuthenticatorAttachmentPlattform
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM:
return domain.AuthenticatorAttachmentCrossPlattform
default:
return domain.AuthenticatorAttachmentUnspecified
}
}
func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*object_pb.Details, *structpb.Struct, error) {
if err != nil {
return nil, nil, err
}
options := new(structpb.Struct)
if err := options.UnmarshalJSON(details.PublicKeyCredentialCreationOptions); err != nil {
return nil, nil, zerrors.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal")
}
return object.DomainToDetailsPb(details.ObjectDetails), options, nil
}
func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
if err != nil {
return nil, err
}
return &user.RegisterPasskeyResponse{
Details: objectDetails,
PasskeyId: details.ID,
PublicKeyCredentialCreationOptions: options,
}, nil
}
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
if err != nil {
return nil, zerrors.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
}
objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), "", req.GetPasskeyName(), "", pkc)
if err != nil {
return nil, err
}
return &user.VerifyPasskeyRegistrationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}
func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) {
switch medium := req.Medium.(type) {
case nil:
return passkeyDetailsToPb(
s.command.AddUserPasskeyCode(ctx, req.GetUserId(), "", s.userCodeAlg),
)
case *user.CreatePasskeyRegistrationLinkRequest_SendLink:
return passkeyDetailsToPb(
s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()),
)
case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode:
return passkeyCodeDetailsToPb(
s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), "", s.userCodeAlg),
)
default:
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium)
}
}
func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
if err != nil {
return nil, err
}
return &user.CreatePasskeyRegistrationLinkResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
if err != nil {
return nil, err
}
return &user.CreatePasskeyRegistrationLinkResponse{
Details: object.DomainToDetailsPb(details.ObjectDetails),
Code: &user.PasskeyRegistrationCode{
Id: details.CodeID,
Code: details.Code,
},
}, nil
}

View File

@ -0,0 +1,319 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_RegisterPasskey(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
require.NoError(t, err)
// We also need a user session
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
type args struct {
ctx context.Context
req *user.RegisterPasskeyRequest
}
tests := []struct {
name string
args args
want *user.RegisterPasskeyResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{},
},
wantErr: true,
},
{
name: "register code",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
},
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "reuse code (not allowed)",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
},
},
wantErr: true,
},
{
name: "wrong code",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
Code: &user.PasskeyRegistrationCode{
Id: reg.GetCode().GetId(),
Code: "foobar",
},
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
},
},
wantErr: true,
},
{
name: "user mismatch",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "user setting its own passkey",
args: args{
ctx: Tester.WithAuthorizationToken(CTX, sessionToken),
req: &user.RegisterPasskeyRequest{
UserId: userID,
},
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RegisterPasskey(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
if tt.want != nil {
assert.NotEmpty(t, got.GetPasskeyId())
assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions())
_, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
}
})
}
}
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
require.NoError(t, err)
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
})
require.NoError(t, err)
require.NotEmpty(t, pkr.GetPasskeyId())
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.VerifyPasskeyRegistrationRequest
}
tests := []struct {
name string
args args
want *user.VerifyPasskeyRegistrationResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.VerifyPasskeyRegistrationRequest{
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: attestationResponse,
PasskeyName: "nice name",
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: CTX,
req: &user.VerifyPasskeyRegistrationRequest{
UserId: userID,
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: attestationResponse,
PasskeyName: "nice name",
},
},
want: &user.VerifyPasskeyRegistrationResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "wrong credential",
args: args{
ctx: CTX,
req: &user.VerifyPasskeyRegistrationRequest{
UserId: userID,
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: &structpb.Struct{
Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}},
},
PasskeyName: "nice name",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.VerifyPasskeyRegistration(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
type args struct {
ctx context.Context
req *user.CreatePasskeyRegistrationLinkRequest
}
tests := []struct {
name string
args args
want *user.CreatePasskeyRegistrationLinkResponse
wantCode bool
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{},
},
wantErr: true,
},
{
name: "send default mail",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
},
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "send custom url",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_SendLink{
SendLink: &user.SendPasskeyRegistrationLink{
UrlTemplate: gu.Ptr("https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}"),
},
},
},
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "return code",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
},
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
wantCode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreatePasskeyRegistrationLink(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
if tt.wantCode {
assert.NotEmpty(t, got.GetCode().GetId())
assert.NotEmpty(t, got.GetCode().GetId())
}
})
}
}

View File

@ -0,0 +1,235 @@
package user
import (
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func Test_passkeyAuthenticatorToDomain(t *testing.T) {
tests := []struct {
pa user.PasskeyAuthenticator
want domain.AuthenticatorAttachment
}{
{
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED,
want: domain.AuthenticatorAttachmentUnspecified,
},
{
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
want: domain.AuthenticatorAttachmentPlattform,
},
{
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
want: domain.AuthenticatorAttachmentCrossPlattform,
},
{
pa: 999,
want: domain.AuthenticatorAttachmentUnspecified,
},
}
for _, tt := range tests {
t.Run(tt.pa.String(), func(t *testing.T) {
got := passkeyAuthenticatorToDomain(tt.pa)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
type args struct {
details *domain.WebAuthNRegistrationDetails
err error
}
tests := []struct {
name string
args args
want *user.RegisterPasskeyResponse
wantErr error
}{
{
name: "an error",
args: args{
details: nil,
err: io.ErrClosedPipe,
},
wantErr: io.ErrClosedPipe,
},
{
name: "unmarshall error",
args: args{
details: &domain.WebAuthNRegistrationDetails{
ObjectDetails: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
ID: "123",
PublicKeyCredentialCreationOptions: []byte(`\\`),
},
err: nil,
},
wantErr: zerrors.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"),
},
{
name: "ok",
args: args{
details: &domain.WebAuthNRegistrationDetails{
ObjectDetails: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
ID: "123",
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
},
err: nil,
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{
Seconds: 3000,
Nanos: 22,
},
ResourceOwner: "me",
},
PasskeyId: "123",
PublicKeyCredentialCreationOptions: &structpb.Struct{
Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.wantErr)
if !proto.Equal(tt.want, got) {
t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got)
}
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
}
})
}
}
func Test_passkeyDetailsToPb(t *testing.T) {
type args struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
args args
want *user.CreatePasskeyRegistrationLinkResponse
}{
{
name: "an error",
args: args{
details: nil,
err: io.ErrClosedPipe,
},
},
{
name: "ok",
args: args{
details: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
err: nil,
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{
Seconds: 3000,
Nanos: 22,
},
ResourceOwner: "me",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.args.err)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passkeyCodeDetailsToPb(t *testing.T) {
type args struct {
details *domain.PasskeyCodeDetails
err error
}
tests := []struct {
name string
args args
want *user.CreatePasskeyRegistrationLinkResponse
}{
{
name: "an error",
args: args{
details: nil,
err: io.ErrClosedPipe,
},
},
{
name: "ok",
args: args{
details: &domain.PasskeyCodeDetails{
ObjectDetails: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
CodeID: "123",
Code: "456",
},
err: nil,
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{
Seconds: 3000,
Nanos: 22,
},
ResourceOwner: "me",
},
Code: &user.PasskeyRegistrationCode{
Id: "123",
Code: "456",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyCodeDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.args.err)
assert.Equal(t, tt.want, got)
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
}
})
}
}

View File

@ -0,0 +1,69 @@
package user
import (
"context"
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) {
var details *domain.ObjectDetails
var code *string
switch m := req.GetMedium().(type) {
case *user.PasswordResetRequest_SendLink:
details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType()))
case *user.PasswordResetRequest_ReturnCode:
details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.GetUserId())
case nil:
details, code, err = s.command.RequestPasswordReset(ctx, req.GetUserId())
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-SDeeg", "verification oneOf %T in method RequestPasswordReset not implemented", m)
}
if err != nil {
return nil, err
}
return &user.PasswordResetResponse{
Details: object.DomainToDetailsPb(details),
VerificationCode: code,
}, nil
}
func notificationTypeToDomain(notificationType user.NotificationType) domain.NotificationType {
switch notificationType {
case user.NotificationType_NOTIFICATION_TYPE_Email:
return domain.NotificationTypeEmail
case user.NotificationType_NOTIFICATION_TYPE_SMS:
return domain.NotificationTypeSms
case user.NotificationType_NOTIFICATION_TYPE_Unspecified:
return domain.NotificationTypeEmail
default:
return domain.NotificationTypeEmail
}
}
func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) {
var details *domain.ObjectDetails
switch v := req.GetVerification().(type) {
case *user.SetPasswordRequest_CurrentPassword:
details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
case *user.SetPasswordRequest_VerificationCode:
details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
case nil:
details, err = s.command.SetPassword(ctx, "", req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired())
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-SFdf2", "verification oneOf %T in method SetPasswordRequest not implemented", v)
}
if err != nil {
return nil, err
}
return &user.SetPasswordResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}

View File

@ -0,0 +1,232 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_RequestPasswordReset(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
tests := []struct {
name string
req *user.PasswordResetRequest
want *user.PasswordResetResponse
wantErr bool
}{
{
name: "default medium",
req: &user.PasswordResetRequest{
UserId: userID,
},
want: &user.PasswordResetResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "custom url template",
req: &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_SendLink{
SendLink: &user.SendPasswordResetLink{
NotificationType: user.NotificationType_NOTIFICATION_TYPE_Email,
UrlTemplate: gu.Ptr("https://example.com/password/change?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
},
},
},
want: &user.PasswordResetResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "template error",
req: &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_SendLink{
SendLink: &user.SendPasswordResetLink{
UrlTemplate: gu.Ptr("{{"),
},
},
},
wantErr: true,
},
{
name: "return code",
req: &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_ReturnCode{
ReturnCode: &user.ReturnPasswordResetCode{},
},
},
want: &user.PasswordResetResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
VerificationCode: gu.Ptr("xxx"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.PasswordReset(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetVerificationCode() != "" {
assert.NotEmpty(t, got.GetVerificationCode())
}
})
}
}
func TestServer_SetPassword(t *testing.T) {
type args struct {
ctx context.Context
req *user.SetPasswordRequest
}
tests := []struct {
name string
prepare func(request *user.SetPasswordRequest) error
args args
want *user.SetPasswordResponse
wantErr bool
}{
{
name: "missing user id",
prepare: func(request *user.SetPasswordRequest) error {
return nil
},
args: args{
ctx: CTX,
req: &user.SetPasswordRequest{},
},
wantErr: true,
},
{
name: "set successful",
prepare: func(request *user.SetPasswordRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
return nil
},
args: args{
ctx: CTX,
req: &user.SetPasswordRequest{
NewPassword: &user.Password{
Password: "Secr3tP4ssw0rd!",
},
},
},
want: &user.SetPasswordResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "change successful",
prepare: func(request *user.SetPasswordRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
_, err := Client.SetPassword(CTX, &user.SetPasswordRequest{
UserId: userID,
NewPassword: &user.Password{
Password: "InitialPassw0rd!",
},
})
return err
},
args: args{
ctx: CTX,
req: &user.SetPasswordRequest{
NewPassword: &user.Password{
Password: "Secr3tP4ssw0rd!",
},
Verification: &user.SetPasswordRequest_CurrentPassword{
CurrentPassword: "InitialPassw0rd!",
},
},
},
want: &user.SetPasswordResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "set with code successful",
prepare: func(request *user.SetPasswordRequest) error {
userID := Tester.CreateHumanUser(CTX).GetUserId()
request.UserId = userID
resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{
UserId: userID,
Medium: &user.PasswordResetRequest_ReturnCode{
ReturnCode: &user.ReturnPasswordResetCode{},
},
})
if err != nil {
return err
}
request.Verification = &user.SetPasswordRequest_VerificationCode{
VerificationCode: resp.GetVerificationCode(),
}
return nil
},
args: args{
ctx: CTX,
req: &user.SetPasswordRequest{
NewPassword: &user.Password{
Password: "Secr3tP4ssw0rd!",
},
},
},
want: &user.SetPasswordResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.SetPassword(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,39 @@
package user
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func Test_notificationTypeToDomain(t *testing.T) {
tests := []struct {
name string
notificationType user.NotificationType
want domain.NotificationType
}{
{
"unspecified",
user.NotificationType_NOTIFICATION_TYPE_Unspecified,
domain.NotificationTypeEmail,
},
{
"email",
user.NotificationType_NOTIFICATION_TYPE_Email,
domain.NotificationTypeEmail,
},
{
"sms",
user.NotificationType_NOTIFICATION_TYPE_SMS,
domain.NotificationTypeSms,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, notificationTypeToDomain(tt.notificationType), "notificationTypeToDomain(%v)", tt.notificationType)
})
}
}

View File

@ -0,0 +1,102 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {
var phone *domain.Phone
switch v := req.GetVerification().(type) {
case *user.SetPhoneRequest_SendCode:
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
case *user.SetPhoneRequest_ReturnCode:
phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
case *user.SetPhoneRequest_IsVerified:
phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), req.GetPhone())
case nil:
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v)
}
if err != nil {
return nil, err
}
return &user.SetPhoneResponse{
Details: &object.Details{
Sequence: phone.Sequence,
ChangeDate: timestamppb.New(phone.ChangeDate),
ResourceOwner: phone.ResourceOwner,
},
VerificationCode: phone.PlainCode,
}, nil
}
func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) {
details, err := s.command.RemoveUserPhone(ctx,
req.GetUserId(),
)
if err != nil {
return nil, err
}
return &user.RemovePhoneResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}
func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) {
var phone *domain.Phone
switch v := req.GetVerification().(type) {
case *user.ResendPhoneCodeRequest_SendCode:
phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg)
case *user.ResendPhoneCodeRequest_ReturnCode:
phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.GetUserId(), s.userCodeAlg)
case nil:
phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg)
default:
err = zerrors.ThrowUnimplementedf(nil, "USERv2-ResendUserPhoneCode", "verification oneOf %T in method SetPhone not implemented", v)
}
if err != nil {
return nil, err
}
return &user.ResendPhoneCodeResponse{
Details: &object.Details{
Sequence: phone.Sequence,
ChangeDate: timestamppb.New(phone.ChangeDate),
ResourceOwner: phone.ResourceOwner,
},
VerificationCode: phone.PlainCode,
}, nil
}
func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) {
details, err := s.command.VerifyUserPhone(ctx,
req.GetUserId(),
req.GetVerificationCode(),
s.userCodeAlg,
)
if err != nil {
return nil, err
}
return &user.VerifyPhoneResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}

View File

@ -0,0 +1,344 @@
//go:build integration
package user_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func TestServer_SetPhone(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
tests := []struct {
name string
req *user.SetPhoneRequest
want *user.SetPhoneResponse
wantErr bool
}{
{
name: "default verification",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234568",
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "send verification",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234569",
Verification: &user.SetPhoneRequest_SendCode{
SendCode: &user.SendPhoneVerificationCode{},
},
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "return code",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234566",
Verification: &user.SetPhoneRequest_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
VerificationCode: gu.Ptr("xxx"),
},
},
{
name: "is verified true",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234565",
Verification: &user.SetPhoneRequest_IsVerified{
IsVerified: true,
},
},
want: &user.SetPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "is verified false",
req: &user.SetPhoneRequest{
UserId: userID,
Phone: "+41791234564",
Verification: &user.SetPhoneRequest_IsVerified{
IsVerified: false,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.SetPhone(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetVerificationCode() != "" {
assert.NotEmpty(t, got.GetVerificationCode())
}
})
}
}
func TestServer_ResendPhoneCode(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId()
tests := []struct {
name string
req *user.ResendPhoneCodeRequest
want *user.ResendPhoneCodeResponse
wantErr bool
}{
{
name: "user not existing",
req: &user.ResendPhoneCodeRequest{
UserId: "xxx",
},
wantErr: true,
},
{
name: "user not existing",
req: &user.ResendPhoneCodeRequest{
UserId: verifiedUserID,
},
wantErr: true,
},
{
name: "resend code",
req: &user.ResendPhoneCodeRequest{
UserId: userID,
Verification: &user.ResendPhoneCodeRequest_SendCode{
SendCode: &user.SendPhoneVerificationCode{},
},
},
want: &user.ResendPhoneCodeResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "return code",
req: &user.ResendPhoneCodeRequest{
UserId: userID,
Verification: &user.ResendPhoneCodeRequest_ReturnCode{
ReturnCode: &user.ReturnPhoneVerificationCode{},
},
},
want: &user.ResendPhoneCodeResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
VerificationCode: gu.Ptr("xxx"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.ResendPhoneCode(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetVerificationCode() != "" {
assert.NotEmpty(t, got.GetVerificationCode())
}
})
}
}
func TestServer_VerifyPhone(t *testing.T) {
userResp := Tester.CreateHumanUser(CTX)
tests := []struct {
name string
req *user.VerifyPhoneRequest
want *user.VerifyPhoneResponse
wantErr bool
}{
{
name: "wrong code",
req: &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: "xxx",
},
wantErr: true,
},
{
name: "wrong user",
req: &user.VerifyPhoneRequest{
UserId: "xxx",
VerificationCode: userResp.GetPhoneCode(),
},
wantErr: true,
},
{
name: "verify user",
req: &user.VerifyPhoneRequest{
UserId: userResp.GetUserId(),
VerificationCode: userResp.GetPhoneCode(),
},
want: &user.VerifyPhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.VerifyPhone(CTX, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_RemovePhone(t *testing.T) {
userResp := Tester.CreateHumanUser(CTX)
failResp := Tester.CreateHumanUserNoPhone(CTX)
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
doubleRemoveUser := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, otherUser)
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
tests := []struct {
name string
ctx context.Context
req *user.RemovePhoneRequest
want *user.RemovePhoneResponse
wantErr bool
dep func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error)
}{
{
name: "remove phone",
ctx: CTX,
req: &user.RemovePhoneRequest{
UserId: userResp.GetUserId(),
},
want: &user.RemovePhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
{
name: "user without phone",
ctx: CTX,
req: &user.RemovePhoneRequest{
UserId: failResp.GetUserId(),
},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
{
name: "remove previously deleted phone",
ctx: CTX,
req: &user.RemovePhoneRequest{
UserId: doubleRemoveUser.GetUserId(),
},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return Client.RemovePhone(ctx, &user.RemovePhoneRequest{
UserId: doubleRemoveUser.GetUserId(),
})
},
},
{
name: "no user id",
ctx: CTX,
req: &user.RemovePhoneRequest{},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
{
name: "other user, no permission",
ctx: Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser),
req: &user.RemovePhoneRequest{
UserId: userResp.GetUserId(),
},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, depErr := tt.dep(tt.ctx, tt.req.UserId)
require.NoError(t, depErr)
got, err := Client.RemovePhone(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}

Some files were not shown because too many files have changed in this diff Show More