mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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 <[email protected]>
This commit is contained in:
co-authored by
Elio Bischof
parent
bc16962aac
commit
7d2d85f57c
@@ -38,6 +38,6 @@ func VerifyDatabase(databaseName string) func(*database.DB) error {
|
|||||||
return func(db *database.DB) error {
|
return func(db *database.DB) error {
|
||||||
logging.WithFields("database", databaseName).Info("verify database")
|
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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ func createEncryptionKeys(ctx context.Context, db *database.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err = tx.Exec(createEncryptionKeysStmt); err != nil {
|
if _, err = tx.Exec(createEncryptionKeysStmt); err != nil {
|
||||||
tx.Rollback()
|
rollbackErr := tx.Rollback()
|
||||||
|
logging.OnError(rollbackErr).Error("rollback failed")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ func createEvents(ctx context.Context, db *database.DB) (err error) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rollbackErr := tx.Rollback()
|
rollbackErr := tx.Rollback()
|
||||||
logging.OnError(rollbackErr).Debug("rollback failed")
|
logging.OnError(rollbackErr).Error("rollback failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package key
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -42,7 +41,7 @@ func MasterKey(cmd *cobra.Command) (string, error) {
|
|||||||
if masterKeyFromEnv {
|
if masterKeyFromEnv {
|
||||||
return os.Getenv(envMasterKey), nil
|
return os.Getenv(envMasterKey), nil
|
||||||
}
|
}
|
||||||
data, err := ioutil.ReadFile(masterKeyFile)
|
data, err := os.ReadFile(masterKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-9
@@ -37,15 +37,21 @@ import (
|
|||||||
action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/action/v3alpha"
|
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/admin"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/auth"
|
"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"
|
"github.com/zitadel/zitadel/internal/api/grpc/management"
|
||||||
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
|
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/settings/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"
|
"github.com/zitadel/zitadel/internal/api/grpc/system"
|
||||||
user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha"
|
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_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"
|
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||||
"github.com/zitadel/zitadel/internal/api/idp"
|
"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 {
|
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil {
|
||||||
return nil, err
|
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 {
|
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
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
|
||||||
if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
|
|
||||||
return nil, err
|
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
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
|
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)
|
apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix)
|
||||||
|
|
||||||
// After OIDC provider so that the callback endpoint can be used
|
// 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 {
|
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: APIs V2 (General Available)
|
title: APIs V2 (Generally Available)
|
||||||
---
|
---
|
||||||
|
|
||||||
import DocCardList from '@theme/DocCardList';
|
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.
|
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.
|
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 />
|
<DocCardList />
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
"start": "docusaurus start",
|
"start": "docusaurus start",
|
||||||
"start:api": "yarn run generate && 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",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
"clear": "docusaurus clear",
|
"clear": "docusaurus clear",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
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 {
|
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
|
||||||
@@ -69,7 +69,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
|
|||||||
ConditionType: &action.Condition_Request{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Service{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Service{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Service{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Service{
|
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{
|
ConditionType: &action.Condition_Response{
|
||||||
Response: &action.ResponseExecution{
|
Response: &action.ResponseExecution{
|
||||||
Condition: &action.ResponseExecution_Method{
|
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{
|
ConditionType: &action.Condition_Response{
|
||||||
Response: &action.ResponseExecution{
|
Response: &action.ResponseExecution{
|
||||||
Condition: &action.ResponseExecution_Method{
|
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{
|
ConditionType: &action.Condition_Response{
|
||||||
Response: &action.ResponseExecution{
|
Response: &action.ResponseExecution{
|
||||||
Condition: &action.ResponseExecution_Service{
|
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{
|
ConditionType: &action.Condition_Response{
|
||||||
Response: &action.ResponseExecution{
|
Response: &action.ResponseExecution{
|
||||||
Condition: &action.ResponseExecution_Method{
|
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{
|
ConditionType: &action.Condition_Response{
|
||||||
Response: &action.ResponseExecution{
|
Response: &action.ResponseExecution{
|
||||||
Condition: &action.ResponseExecution_Method{
|
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{
|
ConditionType: &action.Condition_Response{
|
||||||
Response: &action.ResponseExecution{
|
Response: &action.ResponseExecution{
|
||||||
Condition: &action.ResponseExecution_Service{
|
Condition: &action.ResponseExecution_Service{
|
||||||
Service: "zitadel.user.v2beta.UserService",
|
Service: "zitadel.user.v2.UserService",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
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) {
|
func TestServer_GetTargetByID(t *testing.T) {
|
||||||
@@ -532,7 +532,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
|||||||
ConditionType: &action.Condition_Request{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
ConditionType: &action.Condition_Request{
|
||||||
Request: &action.RequestExecution{
|
Request: &action.RequestExecution{
|
||||||
Condition: &action.RequestExecution_Method{
|
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{
|
Query: &action.SearchQuery_InConditionsQuery{
|
||||||
InConditionsQuery: &action.InConditionsQuery{
|
InConditionsQuery: &action.InConditionsQuery{
|
||||||
Conditions: []*action.Condition{
|
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_Method{Method: "/zitadel.session.v2.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_Service{Service: "zitadel.session.v2.SessionService"}}}},
|
||||||
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
|
{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_Method{Method: "/zitadel.session.v2.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_Service{Service: "zitadel.session.v2.SessionService"}}}},
|
||||||
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
|
{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_Event{Event: "user.added"}}}},
|
||||||
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
|
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
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 (
|
var (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
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) {
|
func TestServer_CreateTarget(t *testing.T) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/feature"
|
"github.com/zitadel/zitadel/internal/feature"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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 {
|
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/feature"
|
"github.com/zitadel/zitadel/internal/feature"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_systemFeaturesToCommand(t *testing.T) {
|
func Test_systemFeaturesToCommand(t *testing.T) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"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) {
|
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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 {
|
type Server struct {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: ×tamppb.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: ×tamppb.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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 {
|
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"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) {
|
func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) {
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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) {
|
func Test_authRequestToPb(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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)
|
var _ oidc_pb.OIDCServiceServer = (*Server)(nil)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("[email protected]"),
|
||||||
|
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("[email protected]"),
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"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) {
|
func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_addOrganizationRequestToCommand(t *testing.T) {
|
func Test_addOrganizationRequestToCommand(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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)
|
var _ org.OrganizationServiceServer = (*Server)(nil)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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("%[email protected]", 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("%[email protected]", 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 protected]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Roles: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &command.OrgSetup{
|
||||||
|
Name: "name",
|
||||||
|
CustomDomain: "",
|
||||||
|
Admins: []*command.OrgSetupAdmin{
|
||||||
|
{
|
||||||
|
Human: &command.AddHuman{
|
||||||
|
Username: "[email protected]",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: command.Email{
|
||||||
|
Address: "[email protected]",
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ func ActivityInterceptor() grpc.UnaryServerInterceptor {
|
|||||||
var resourcePrefixes = []string{
|
var resourcePrefixes = []string{
|
||||||
"/zitadel.management.v1.ManagementService/",
|
"/zitadel.management.v1.ManagementService/",
|
||||||
"/zitadel.admin.v1.AdminService/",
|
"/zitadel.admin.v1.AdminService/",
|
||||||
|
"/zitadel.user.v2.UserService/",
|
||||||
|
"/zitadel.settings.v2.SettingsService/",
|
||||||
"/zitadel.user.v2beta.UserService/",
|
"/zitadel.user.v2beta.UserService/",
|
||||||
"/zitadel.settings.v2beta.SettingsService/",
|
"/zitadel.settings.v2beta.SettingsService/",
|
||||||
"/zitadel.auth.v1.AuthService/",
|
"/zitadel.auth.v1.AuthService/",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -153,7 +153,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -181,7 +181,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -211,7 +211,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
@@ -240,7 +240,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
@@ -264,7 +264,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -293,7 +293,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeAsync,
|
TargetType: domain.TargetTypeAsync,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
@@ -321,7 +321,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeAsync,
|
TargetType: domain.TargetTypeAsync,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -349,7 +349,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeWebhook,
|
TargetType: domain.TargetTypeWebhook,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -377,7 +377,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeWebhook,
|
TargetType: domain.TargetTypeWebhook,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
@@ -406,7 +406,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeWebhook,
|
TargetType: domain.TargetTypeWebhook,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -435,7 +435,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target1",
|
TargetID: "target1",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -443,7 +443,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target2",
|
TargetID: "target2",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -451,7 +451,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target3",
|
TargetID: "target3",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -493,7 +493,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target1",
|
TargetID: "target1",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -501,7 +501,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target2",
|
TargetID: "target2",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
@@ -509,7 +509,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target3",
|
TargetID: "target3",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
@@ -687,7 +687,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
@@ -716,7 +716,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
|||||||
executionTargets: []execution.Target{
|
executionTargets: []execution.Target{
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
ExecutionID: "response./zitadel.session.v2beta.SessionService/SetSession",
|
ExecutionID: "response./zitadel.session.v2.SessionService/SetSession",
|
||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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)
|
var _ session.SessionServiceServer = (*Server)(nil)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
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 (
|
var (
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -860,7 +860,7 @@ func TestServer_SetSession_expired(t *testing.T) {
|
|||||||
|
|
||||||
// ensure session expires and does not work anymore
|
// ensure session expires and does not work anymore
|
||||||
time.Sleep(20 * time.Second)
|
time.Sleep(20 * time.Second)
|
||||||
_, err = Tester.Client.SessionV2.SetSession(CTX, &session.SetSessionRequest{
|
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||||
SessionId: createResp.GetSessionId(),
|
SessionId: createResp.GetSessionId(),
|
||||||
Lifetime: durationpb.New(20 * time.Second),
|
Lifetime: durationpb.New(20 * time.Second),
|
||||||
})
|
})
|
||||||
@@ -944,7 +944,7 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
|
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.Error(t, err)
|
||||||
require.Nil(t, sessionResp)
|
require.Nil(t, sessionResp)
|
||||||
}
|
}
|
||||||
@@ -953,7 +953,7 @@ func Test_ZITADEL_API_success(t *testing.T) {
|
|||||||
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
|
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
|
||||||
|
|
||||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
|
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
|
||||||
@@ -966,17 +966,17 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
|
|||||||
|
|
||||||
// test session token works
|
// test session token works
|
||||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
//terminate the session and test it does not work anymore
|
//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,
|
SessionId: id,
|
||||||
SessionToken: gu.Ptr(token),
|
SessionToken: gu.Ptr(token),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
ctx = Tester.WithAuthorizationToken(context.Background(), token)
|
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)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,12 +985,12 @@ func Test_ZITADEL_API_session_expired(t *testing.T) {
|
|||||||
|
|
||||||
// test session token works
|
// test session token works
|
||||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// ensure session expires and does not work anymore
|
// ensure session expires and does not work anymore
|
||||||
time.Sleep(20 * time.Second)
|
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.Error(t, err)
|
||||||
require.Nil(t, sessionResp)
|
require.Nil(t, sessionResp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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("%[email protected]", 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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)
|
var _ settings.SettingsServiceServer = (*Server)(nil)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/i18n"
|
"github.com/zitadel/zitadel/internal/i18n"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {
|
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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 {
|
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/database"
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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"}
|
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_GetSecuritySettings(t *testing.T) {
|
func TestServer_GetSecuritySettings(t *testing.T) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}())
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: "[email protected]",
|
||||||
|
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: "[email protected]",
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
|
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 {
|
func TypeToPb(userType domain.UserType) user_pb.Type {
|
||||||
switch userType {
|
switch userType {
|
||||||
case domain.UserTypeHuman:
|
case domain.UserTypeHuman:
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
|
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_SetEmail(t *testing.T) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("%[email protected]", time.Now().UnixNano()))
|
||||||
|
|
||||||
|
instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX)
|
||||||
|
userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%[email protected]", 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("%[email protected]", time.Now().UnixNano()))
|
||||||
|
Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org")
|
||||||
|
|
||||||
|
userMultipleResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%[email protected]", 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("%[email protected]", time.Now().UnixNano()))
|
||||||
|
|
||||||
|
instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX)
|
||||||
|
userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%[email protected]", 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("%[email protected]", time.Now().UnixNano()))
|
||||||
|
Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org")
|
||||||
|
|
||||||
|
userNoLinkResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%[email protected]", 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"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) {
|
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_AddOTPSMS(t *testing.T) {
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
|
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
|
}, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package user_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -12,9 +13,10 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_RegisterPasskey(t *testing.T) {
|
||||||
@@ -138,19 +140,7 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
|
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
|
||||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
userID, pkr := userWithPasskeyRegistered(t)
|
||||||
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())
|
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||||
require.NoError(t, err)
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"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) {
|
func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) {
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_RequestPasswordReset(t *testing.T) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"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) {
|
func Test_notificationTypeToDomain(t *testing.T) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {
|
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_SetPhone(t *testing.T) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"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) {
|
func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) {
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_GetUserByID(t *testing.T) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"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)
|
var _ user.UserServiceServer = (*Server)(nil)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"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) {
|
func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) {
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_RegisterTOTP(t *testing.T) {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_totpDetailsToPb(t *testing.T) {
|
func Test_totpDetailsToPb(t *testing.T) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"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) {
|
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),
|
Details: object.DomainToDetailsPb(objectDetails),
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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"
|
"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) {
|
func TestServer_RegisterU2F(t *testing.T) {
|
||||||
@@ -106,16 +107,7 @@ func TestServer_RegisterU2F(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_VerifyU2FRegistration(t *testing.T) {
|
func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t)
|
||||||
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())
|
|
||||||
|
|
||||||
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||||
require.NoError(t, err)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||||
"github.com/zitadel/zitadel/internal/query"
|
"github.com/zitadel/zitadel/internal/query"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
||||||
|
|
||||||
human, err := AddUserRequestToAddHuman(req)
|
human, err := AddUserRequestToAddHuman(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
|
||||||
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
|
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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/api/grpc"
|
||||||
"github.com/zitadel/zitadel/internal/integration"
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/idp"
|
"github.com/zitadel/zitadel/pkg/grpc/idp"
|
||||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
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 (
|
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) {
|
func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||||
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, Tester.Organisation.ID)
|
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, Tester.Organisation.ID)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_idpIntentToIDPIntentPb(t *testing.T) {
|
func Test_idpIntentToIDPIntentPb(t *testing.T) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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: "[email protected]",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default verfication",
|
||||||
|
req: &user.SetEmailRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Email: "[email protected]",
|
||||||
|
},
|
||||||
|
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: "[email protected]",
|
||||||
|
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: "[email protected]",
|
||||||
|
Verification: &user.SetEmailRequest_SendCode{
|
||||||
|
SendCode: &user.SendEmailVerificationCode{
|
||||||
|
UrlTemplate: gu.Ptr("{{"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return code",
|
||||||
|
req: &user.SetEmailRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Email: "[email protected]",
|
||||||
|
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: "[email protected]",
|
||||||
|
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: "[email protected]",
|
||||||
|
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("%[email protected]", 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: ×tamppb.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: ×tamppb.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: ×tamppb.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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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("%[email protected]", 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
Reference in New Issue
Block a user