mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-26 13:28:41 +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 <elio@zitadel.com>
This commit is contained in:
parent
bc16962aac
commit
7d2d85f57c
@ -38,6 +38,6 @@ func VerifyDatabase(databaseName string) func(*database.DB) error {
|
||||
return func(db *database.DB) error {
|
||||
logging.WithFields("database", databaseName).Info("verify database")
|
||||
|
||||
return exec(db, fmt.Sprintf(string(databaseStmt), databaseName), []string{dbAlreadyExistsCode})
|
||||
return exec(db, fmt.Sprintf(databaseStmt, databaseName), []string{dbAlreadyExistsCode})
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,8 @@ func createEncryptionKeys(ctx context.Context, db *database.DB) error {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(createEncryptionKeysStmt); err != nil {
|
||||
tx.Rollback()
|
||||
rollbackErr := tx.Rollback()
|
||||
logging.OnError(rollbackErr).Error("rollback failed")
|
||||
return err
|
||||
}
|
||||
|
||||
@ -110,7 +111,7 @@ func createEvents(ctx context.Context, db *database.DB) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
rollbackErr := tx.Rollback()
|
||||
logging.OnError(rollbackErr).Debug("rollback failed")
|
||||
logging.OnError(rollbackErr).Error("rollback failed")
|
||||
return
|
||||
}
|
||||
err = tx.Commit()
|
||||
|
@ -2,7 +2,6 @@ package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@ -42,7 +41,7 @@ func MasterKey(cmd *cobra.Command) (string, error) {
|
||||
if masterKeyFromEnv {
|
||||
return os.Getenv(envMasterKey), nil
|
||||
}
|
||||
data, err := ioutil.ReadFile(masterKeyFile)
|
||||
data, err := os.ReadFile(masterKeyFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -37,15 +37,21 @@ import (
|
||||
action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/action/v3alpha"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/admin"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/auth"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
|
||||
feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
|
||||
feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/management"
|
||||
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
||||
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
|
||||
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
||||
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta"
|
||||
session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
|
||||
session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
|
||||
settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
|
||||
settings_v2beta "github.com/zitadel/zitadel/internal/api/grpc/settings/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/system"
|
||||
user_schema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/user/schema/v3alpha"
|
||||
user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2"
|
||||
user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/idp"
|
||||
@ -399,20 +405,34 @@ func startAPIs(
|
||||
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
|
||||
if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
|
||||
if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, feature.CreateServer(commands, queries)); err != nil {
|
||||
if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, session_v2.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, settings_v2.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, org_v2.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
|
||||
@ -491,6 +511,9 @@ func startAPIs(
|
||||
apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix)
|
||||
|
||||
// After OIDC provider so that the callback endpoint can be used
|
||||
if err := apis.RegisterService(ctx, oidc_v2beta.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: APIs V2 (General Available)
|
||||
title: APIs V2 (Generally Available)
|
||||
---
|
||||
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
@ -7,4 +7,7 @@ import DocCardList from '@theme/DocCardList';
|
||||
APIs V2 organize access by resources (users, settings, etc.), unlike context-specific V1 APIs.
|
||||
This simplifies finding the right API, especially for multi-organization resources.
|
||||
|
||||
Users created with the V2 API have no initial state anymore, so new users are immediately active.
|
||||
Requesting ZITADEL to send a verification email on user creation is still possible.
|
||||
|
||||
<DocCardList />
|
@ -6,7 +6,7 @@
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"start:api": "yarn run generate && docusaurus start",
|
||||
"build": "yarn run generate && docusaurus build",
|
||||
"build": "yarn run generate && NODE_OPTIONS=--max-old-space-size=6144 docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
)
|
||||
|
||||
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
|
||||
@ -69,7 +69,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.NotExistingService/List",
|
||||
Method: "/zitadel.session.v2.NotExistingService/List",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -86,7 +86,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
|
||||
Method: "/zitadel.session.v2.SessionService/ListSessions",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -125,7 +125,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Service{
|
||||
Service: "zitadel.session.v2beta.SessionService",
|
||||
Service: "zitadel.session.v2.SessionService",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -200,7 +200,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Service{
|
||||
Service: "zitadel.session.v2beta.SessionService",
|
||||
Service: "zitadel.session.v2.SessionService",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -213,7 +213,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
|
||||
Method: "/zitadel.session.v2.SessionService/ListSessions",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -247,7 +247,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
|
||||
Method: "/zitadel.session.v2.SessionService/ListSessions",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -269,7 +269,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Service{
|
||||
Service: "zitadel.session.v2beta.SessionService",
|
||||
Service: "zitadel.session.v2.SessionService",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -347,7 +347,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/NotExisting",
|
||||
Method: "/zitadel.session.v2.SessionService/NotExisting",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -367,7 +367,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/GetSession",
|
||||
Method: "/zitadel.session.v2.SessionService/GetSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -408,7 +408,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Service{
|
||||
Service: "zitadel.user.v2beta.UserService",
|
||||
Service: "zitadel.user.v2.UserService",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -512,7 +512,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
|
||||
ConditionType: &action.Condition_Response{
|
||||
Response: &action.ResponseExecution{
|
||||
Condition: &action.ResponseExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.NotExistingService/List",
|
||||
Method: "/zitadel.session.v2.NotExistingService/List",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -529,7 +529,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
|
||||
ConditionType: &action.Condition_Response{
|
||||
Response: &action.ResponseExecution{
|
||||
Condition: &action.ResponseExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
|
||||
Method: "/zitadel.session.v2.SessionService/ListSessions",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -568,7 +568,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
|
||||
ConditionType: &action.Condition_Response{
|
||||
Response: &action.ResponseExecution{
|
||||
Condition: &action.ResponseExecution_Service{
|
||||
Service: "zitadel.session.v2beta.SessionService",
|
||||
Service: "zitadel.session.v2.SessionService",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -670,7 +670,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
|
||||
ConditionType: &action.Condition_Response{
|
||||
Response: &action.ResponseExecution{
|
||||
Condition: &action.ResponseExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/NotExisting",
|
||||
Method: "/zitadel.session.v2.SessionService/NotExisting",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -690,7 +690,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
|
||||
ConditionType: &action.Condition_Response{
|
||||
Response: &action.ResponseExecution{
|
||||
Condition: &action.ResponseExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/GetSession",
|
||||
Method: "/zitadel.session.v2.SessionService/GetSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -731,7 +731,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
|
||||
ConditionType: &action.Condition_Response{
|
||||
Response: &action.ResponseExecution{
|
||||
Condition: &action.ResponseExecution_Service{
|
||||
Service: "zitadel.user.v2beta.UserService",
|
||||
Service: "zitadel.user.v2.UserService",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
)
|
||||
|
||||
func TestServer_GetTargetByID(t *testing.T) {
|
||||
@ -532,7 +532,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/GetSession",
|
||||
Method: "/zitadel.session.v2.SessionService/GetSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -555,7 +555,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/GetSession",
|
||||
Method: "/zitadel.session.v2.SessionService/GetSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -720,7 +720,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/GetSession",
|
||||
Method: "/zitadel.session.v2.SessionService/GetSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -729,7 +729,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/CreateSession",
|
||||
Method: "/zitadel.session.v2.SessionService/CreateSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -738,7 +738,7 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
ConditionType: &action.Condition_Request{
|
||||
Request: &action.RequestExecution{
|
||||
Condition: &action.RequestExecution_Method{
|
||||
Method: "/zitadel.session.v2beta.SessionService/SetSession",
|
||||
Method: "/zitadel.session.v2.SessionService/SetSession",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -795,11 +795,11 @@ func TestServer_ListExecutions(t *testing.T) {
|
||||
Query: &action.SearchQuery_InConditionsQuery{
|
||||
InConditionsQuery: &action.InConditionsQuery{
|
||||
Conditions: []*action.Condition{
|
||||
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}},
|
||||
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}},
|
||||
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
|
||||
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
|
||||
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
|
||||
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}},
|
||||
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}},
|
||||
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
|
||||
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
|
||||
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
|
||||
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
|
||||
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
)
|
||||
|
||||
func TestServer_CreateTarget(t *testing.T) {
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
)
|
||||
|
||||
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
|
||||
|
@ -12,8 +12,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
)
|
||||
|
||||
func Test_systemFeaturesToCommand(t *testing.T) {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
)
|
||||
|
||||
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
|
||||
|
@ -14,8 +14,8 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
155
internal/api/grpc/feature/v2beta/converter.go
Normal file
155
internal/api/grpc/feature/v2beta/converter.go
Normal file
@ -0,0 +1,155 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures {
|
||||
return &command.SystemFeatures{
|
||||
LoginDefaultOrg: req.LoginDefaultOrg,
|
||||
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
||||
LegacyIntrospection: req.OidcLegacyIntrospection,
|
||||
UserSchema: req.UserSchema,
|
||||
Actions: req.Actions,
|
||||
TokenExchange: req.OidcTokenExchange,
|
||||
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
|
||||
}
|
||||
}
|
||||
|
||||
func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse {
|
||||
return &feature_pb.GetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(f.Details),
|
||||
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
||||
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
||||
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
||||
UserSchema: featureSourceToFlagPb(&f.UserSchema),
|
||||
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
|
||||
Actions: featureSourceToFlagPb(&f.Actions),
|
||||
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
|
||||
}
|
||||
}
|
||||
|
||||
func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures {
|
||||
return &command.InstanceFeatures{
|
||||
LoginDefaultOrg: req.LoginDefaultOrg,
|
||||
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
|
||||
LegacyIntrospection: req.OidcLegacyIntrospection,
|
||||
UserSchema: req.UserSchema,
|
||||
TokenExchange: req.OidcTokenExchange,
|
||||
Actions: req.Actions,
|
||||
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
|
||||
}
|
||||
}
|
||||
|
||||
func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse {
|
||||
return &feature_pb.GetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(f.Details),
|
||||
LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg),
|
||||
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
|
||||
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
|
||||
UserSchema: featureSourceToFlagPb(&f.UserSchema),
|
||||
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
|
||||
Actions: featureSourceToFlagPb(&f.Actions),
|
||||
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
|
||||
}
|
||||
}
|
||||
|
||||
func featureSourceToImprovedPerformanceFlagPb(fs *query.FeatureSource[[]feature.ImprovedPerformanceType]) *feature_pb.ImprovedPerformanceFeatureFlag {
|
||||
return &feature_pb.ImprovedPerformanceFeatureFlag{
|
||||
ExecutionPaths: improvedPerformanceTypesToPb(fs.Value),
|
||||
Source: featureLevelToSourcePb(fs.Level),
|
||||
}
|
||||
}
|
||||
|
||||
func featureSourceToFlagPb(fs *query.FeatureSource[bool]) *feature_pb.FeatureFlag {
|
||||
return &feature_pb.FeatureFlag{
|
||||
Enabled: fs.Value,
|
||||
Source: featureLevelToSourcePb(fs.Level),
|
||||
}
|
||||
}
|
||||
|
||||
func featureLevelToSourcePb(level feature.Level) feature_pb.Source {
|
||||
switch level {
|
||||
case feature.LevelUnspecified:
|
||||
return feature_pb.Source_SOURCE_UNSPECIFIED
|
||||
case feature.LevelSystem:
|
||||
return feature_pb.Source_SOURCE_SYSTEM
|
||||
case feature.LevelInstance:
|
||||
return feature_pb.Source_SOURCE_INSTANCE
|
||||
case feature.LevelOrg:
|
||||
return feature_pb.Source_SOURCE_ORGANIZATION
|
||||
case feature.LevelProject:
|
||||
return feature_pb.Source_SOURCE_PROJECT
|
||||
case feature.LevelApp:
|
||||
return feature_pb.Source_SOURCE_APP
|
||||
case feature.LevelUser:
|
||||
return feature_pb.Source_SOURCE_USER
|
||||
default:
|
||||
return feature_pb.Source(level)
|
||||
}
|
||||
}
|
||||
|
||||
func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []feature_pb.ImprovedPerformance {
|
||||
res := make([]feature_pb.ImprovedPerformance, len(types))
|
||||
|
||||
for i, typ := range types {
|
||||
res[i] = improvedPerformanceTypeToPb(typ)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance {
|
||||
switch typ {
|
||||
case feature.ImprovedPerformanceTypeUnknown:
|
||||
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED
|
||||
case feature.ImprovedPerformanceTypeOrgByID:
|
||||
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID
|
||||
case feature.ImprovedPerformanceTypeProjectGrant:
|
||||
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT
|
||||
case feature.ImprovedPerformanceTypeProject:
|
||||
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT
|
||||
case feature.ImprovedPerformanceTypeUserGrant:
|
||||
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT
|
||||
case feature.ImprovedPerformanceTypeOrgDomainVerified:
|
||||
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED
|
||||
default:
|
||||
return feature_pb.ImprovedPerformance(typ)
|
||||
}
|
||||
}
|
||||
|
||||
func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []feature.ImprovedPerformanceType {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
res := make([]feature.ImprovedPerformanceType, len(list))
|
||||
|
||||
for i, typ := range list {
|
||||
res[i] = improvedPerformanceToDomain(typ)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType {
|
||||
switch typ {
|
||||
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED:
|
||||
return feature.ImprovedPerformanceTypeUnknown
|
||||
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID:
|
||||
return feature.ImprovedPerformanceTypeOrgByID
|
||||
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT:
|
||||
return feature.ImprovedPerformanceTypeProjectGrant
|
||||
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT:
|
||||
return feature.ImprovedPerformanceTypeProject
|
||||
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_USER_GRANT:
|
||||
return feature.ImprovedPerformanceTypeUserGrant
|
||||
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED:
|
||||
return feature.ImprovedPerformanceTypeOrgDomainVerified
|
||||
default:
|
||||
return feature.ImprovedPerformanceTypeUnknown
|
||||
}
|
||||
}
|
268
internal/api/grpc/feature/v2beta/converter_test.go
Normal file
268
internal/api/grpc/feature/v2beta/converter_test.go
Normal file
@ -0,0 +1,268 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature_pb "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
)
|
||||
|
||||
func Test_systemFeaturesToCommand(t *testing.T) {
|
||||
arg := &feature_pb.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
OidcLegacyIntrospection: nil,
|
||||
UserSchema: gu.Ptr(true),
|
||||
Actions: gu.Ptr(true),
|
||||
OidcTokenExchange: gu.Ptr(true),
|
||||
ImprovedPerformance: nil,
|
||||
}
|
||||
want := &command.SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: nil,
|
||||
UserSchema: gu.Ptr(true),
|
||||
Actions: gu.Ptr(true),
|
||||
TokenExchange: gu.Ptr(true),
|
||||
ImprovedPerformance: nil,
|
||||
}
|
||||
got := systemFeaturesToCommand(arg)
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_systemFeaturesToPb(t *testing.T) {
|
||||
arg := &query.SystemFeatures{
|
||||
Details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(123, 0),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
LoginDefaultOrg: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TriggerIntrospectionProjections: query.FeatureSource[bool]{
|
||||
Level: feature.LevelUnspecified,
|
||||
Value: false,
|
||||
},
|
||||
LegacyIntrospection: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
UserSchema: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
Actions: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: true,
|
||||
},
|
||||
TokenExchange: query.FeatureSource[bool]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: false,
|
||||
},
|
||||
ImprovedPerformance: query.FeatureSource[[]feature.ImprovedPerformanceType]{
|
||||
Level: feature.LevelSystem,
|
||||
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
|
||||
},
|
||||
}
|
||||
want := &feature_pb.GetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×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)
|
||||
})
|
||||
}
|
||||
}
|
86
internal/api/grpc/feature/v2beta/feature.go
Normal file
86
internal/api/grpc/feature/v2beta/feature.go
Normal file
@ -0,0 +1,86 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) {
|
||||
details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.SetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetSystemFeatures(ctx context.Context, req *feature.ResetSystemFeaturesRequest) (_ *feature.ResetSystemFeaturesResponse, err error) {
|
||||
details, err := s.command.ResetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.ResetSystemFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFeaturesRequest) (_ *feature.GetSystemFeaturesResponse, err error) {
|
||||
f, err := s.query.GetSystemFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return systemFeaturesToPb(f), nil
|
||||
}
|
||||
|
||||
func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) {
|
||||
details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.SetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResetInstanceFeatures(ctx context.Context, req *feature.ResetInstanceFeaturesRequest) (_ *feature.ResetInstanceFeaturesResponse, err error) {
|
||||
details, err := s.command.ResetInstanceFeatures(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &feature.ResetInstanceFeaturesResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetInstanceFeatures(ctx context.Context, req *feature.GetInstanceFeaturesRequest) (_ *feature.GetInstanceFeaturesResponse, err error) {
|
||||
f, err := s.query.GetInstanceFeatures(ctx, req.GetInheritance())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return instanceFeaturesToPb(f), nil
|
||||
}
|
||||
|
||||
func (s *Server) SetOrganizationFeatures(ctx context.Context, req *feature.SetOrganizationFeaturesRequest) (_ *feature.SetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *feature.ResetOrganizationFeaturesRequest) (_ *feature.ResetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) GetOrganizationFeatures(ctx context.Context, req *feature.GetOrganizationFeaturesRequest) (_ *feature.GetOrganizationFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetOrganizationFeatures not implemented")
|
||||
}
|
||||
func (s *Server) SetUserFeatures(ctx context.Context, req *feature.SetUserFeatureRequest) (_ *feature.SetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetUserFeatures not implemented")
|
||||
}
|
||||
func (s *Server) ResetUserFeatures(ctx context.Context, req *feature.ResetUserFeaturesRequest) (_ *feature.ResetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ResetUserFeatures not implemented")
|
||||
}
|
||||
func (s *Server) GetUserFeatures(ctx context.Context, req *feature.GetUserFeaturesRequest) (_ *feature.GetUserFeaturesResponse, err error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserFeatures not implemented")
|
||||
}
|
499
internal/api/grpc/feature/v2beta/feature_integration_test.go
Normal file
499
internal/api/grpc/feature/v2beta/feature_integration_test.go
Normal file
@ -0,0 +1,499 @@
|
||||
//go:build integration
|
||||
|
||||
package feature_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
SystemCTX context.Context
|
||||
IamCTX context.Context
|
||||
OrgCTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client feature.FeatureServiceClient
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, _, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
Tester = integration.NewTester(ctx)
|
||||
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
|
||||
IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
|
||||
OrgCTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
|
||||
|
||||
defer Tester.Done()
|
||||
Client = Tester.Client.FeatureV2beta
|
||||
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func TestServer_SetSystemFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.SetSystemFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *feature.SetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no changes error",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.SetSystemFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
want: &feature.SetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
got, err := Client.SetSystemFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResetSystemFeatures(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *feature.ResetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
ctx: IamCTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
ctx: SystemCTX,
|
||||
want: &feature.ResetSystemFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResetSystemFeatures(tt.ctx, &feature.ResetSystemFeaturesRequest{})
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetSystemFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.GetSystemFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(t *testing.T)
|
||||
args args
|
||||
want *feature.GetSystemFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nothing set",
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetSystemFeaturesResponse{},
|
||||
},
|
||||
{
|
||||
name: "some features",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: SystemCTX,
|
||||
req: &feature.GetSystemFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetSystemFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if tt.prepare != nil {
|
||||
tt.prepare(t)
|
||||
}
|
||||
got, err := Client.GetSystemFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
|
||||
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
|
||||
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
|
||||
assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
|
||||
assertFeatureFlag(t, tt.want.Actions, got.Actions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_SetInstanceFeatures(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.SetInstanceFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *feature.SetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: OrgCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no changes error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.SetInstanceFeaturesRequest{
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(true),
|
||||
},
|
||||
},
|
||||
want: &feature.SetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
got, err := Client.SetInstanceFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResetInstanceFeatures(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *feature.ResetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
ctx: OrgCTX,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
ctx: IamCTX,
|
||||
want: &feature.ResetInstanceFeaturesResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResetInstanceFeatures(tt.ctx, &feature.ResetInstanceFeaturesRequest{})
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_GetInstanceFeatures(t *testing.T) {
|
||||
_, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{
|
||||
OidcLegacyIntrospection: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_, err := Client.ResetSystemFeatures(SystemCTX, &feature.ResetSystemFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *feature.GetInstanceFeaturesRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(t *testing.T)
|
||||
args args
|
||||
want *feature.GetInstanceFeaturesResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: OrgCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "defaults, no inheritance",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{},
|
||||
},
|
||||
{
|
||||
name: "defaults, inheritance",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{
|
||||
Inheritance: true,
|
||||
},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
UserSchema: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
Actions: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "some features, no inheritance",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
OidcTriggerIntrospectionProjections: gu.Ptr(false),
|
||||
UserSchema: gu.Ptr(true),
|
||||
Actions: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
UserSchema: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
Actions: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one feature, inheritance",
|
||||
prepare: func(t *testing.T) {
|
||||
_, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
},
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &feature.GetInstanceFeaturesRequest{
|
||||
Inheritance: true,
|
||||
},
|
||||
},
|
||||
want: &feature.GetInstanceFeaturesResponse{
|
||||
LoginDefaultOrg: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_INSTANCE,
|
||||
},
|
||||
OidcTriggerIntrospectionProjections: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
OidcLegacyIntrospection: &feature.FeatureFlag{
|
||||
Enabled: true,
|
||||
Source: feature.Source_SOURCE_SYSTEM,
|
||||
},
|
||||
UserSchema: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
Actions: &feature.FeatureFlag{
|
||||
Enabled: false,
|
||||
Source: feature.Source_SOURCE_UNSPECIFIED,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
// make sure we have a clean state after each test
|
||||
_, err := Client.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if tt.prepare != nil {
|
||||
tt.prepare(t)
|
||||
}
|
||||
got, err := Client.GetInstanceFeatures(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg)
|
||||
assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections)
|
||||
assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection)
|
||||
assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertFeatureFlag(t *testing.T, expected, actual *feature.FeatureFlag) {
|
||||
t.Helper()
|
||||
assert.Equal(t, expected.GetEnabled(), actual.GetEnabled(), "enabled")
|
||||
assert.Equal(t, expected.GetSource(), actual.GetSource(), "source")
|
||||
}
|
47
internal/api/grpc/feature/v2beta/server.go
Normal file
47
internal/api/grpc/feature/v2beta/server.go
Normal file
@ -0,0 +1,47 @@
|
||||
package feature
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
feature.UnimplementedFeatureServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
feature.RegisterFeatureServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return feature.FeatureService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return feature.FeatureService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return feature.FeatureService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return feature.RegisterFeatureServiceHandler
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
)
|
||||
|
||||
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||
|
72
internal/api/grpc/object/v2beta/converter.go
Normal file
72
internal/api/grpc/object/v2beta/converter.go
Normal file
@ -0,0 +1,72 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
)
|
||||
|
||||
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
|
||||
details := &object.Details{
|
||||
Sequence: objectDetail.Sequence,
|
||||
ResourceOwner: objectDetail.ResourceOwner,
|
||||
}
|
||||
if !objectDetail.EventDate.IsZero() {
|
||||
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
func ToListDetails(response query.SearchResponse) *object.ListDetails {
|
||||
details := &object.ListDetails{
|
||||
TotalResult: response.Count,
|
||||
ProcessedSequence: response.Sequence,
|
||||
Timestamp: timestamppb.New(response.EventCreatedAt),
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) {
|
||||
if query == nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return query.Offset, uint64(query.Limit), query.Asc
|
||||
}
|
||||
|
||||
func ResourceOwnerFromReq(ctx context.Context, req *object.RequestContext) string {
|
||||
if req.GetInstance() {
|
||||
return authz.GetInstance(ctx).InstanceID()
|
||||
}
|
||||
if req.GetOrgId() != "" {
|
||||
return req.GetOrgId()
|
||||
}
|
||||
return authz.GetCtxData(ctx).OrgID
|
||||
}
|
||||
|
||||
func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison {
|
||||
switch method {
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS:
|
||||
return query.TextEquals
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE:
|
||||
return query.TextEqualsIgnoreCase
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH:
|
||||
return query.TextStartsWith
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_STARTS_WITH_IGNORE_CASE:
|
||||
return query.TextStartsWithIgnoreCase
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS:
|
||||
return query.TextContains
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE:
|
||||
return query.TextContainsIgnoreCase
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH:
|
||||
return query.TextEndsWith
|
||||
case object.TextQueryMethod_TEXT_QUERY_METHOD_ENDS_WITH_IGNORE_CASE:
|
||||
return query.TextEndsWithIgnoreCase
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
)
|
||||
|
||||
func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) {
|
||||
|
@ -16,10 +16,10 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
)
|
||||
|
||||
func Test_authRequestToPb(t *testing.T) {
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
|
||||
)
|
||||
|
||||
var _ oidc_pb.OIDCServiceServer = (*Server)(nil)
|
||||
|
204
internal/api/grpc/oidc/v2beta/oidc.go
Normal file
204
internal/api/grpc/oidc/v2beta/oidc.go
Normal file
@ -0,0 +1,204 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) {
|
||||
authRequest, err := s.query.AuthRequestByID(ctx, true, req.GetAuthRequestId(), true)
|
||||
if err != nil {
|
||||
logging.WithError(err).Error("query authRequest by ID")
|
||||
return nil, err
|
||||
}
|
||||
return &oidc_pb.GetAuthRequestResponse{
|
||||
AuthRequest: authRequestToPb(authRequest),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest {
|
||||
pba := &oidc_pb.AuthRequest{
|
||||
Id: a.ID,
|
||||
CreationDate: timestamppb.New(a.CreationDate),
|
||||
ClientId: a.ClientID,
|
||||
Scope: a.Scope,
|
||||
RedirectUri: a.RedirectURI,
|
||||
Prompt: promptsToPb(a.Prompt),
|
||||
UiLocales: a.UiLocales,
|
||||
LoginHint: a.LoginHint,
|
||||
HintUserId: a.HintUserID,
|
||||
}
|
||||
if a.MaxAge != nil {
|
||||
pba.MaxAge = durationpb.New(*a.MaxAge)
|
||||
}
|
||||
return pba
|
||||
}
|
||||
|
||||
func promptsToPb(promps []domain.Prompt) []oidc_pb.Prompt {
|
||||
out := make([]oidc_pb.Prompt, len(promps))
|
||||
for i, p := range promps {
|
||||
out[i] = promptToPb(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func promptToPb(p domain.Prompt) oidc_pb.Prompt {
|
||||
switch p {
|
||||
case domain.PromptUnspecified:
|
||||
return oidc_pb.Prompt_PROMPT_UNSPECIFIED
|
||||
case domain.PromptNone:
|
||||
return oidc_pb.Prompt_PROMPT_NONE
|
||||
case domain.PromptLogin:
|
||||
return oidc_pb.Prompt_PROMPT_LOGIN
|
||||
case domain.PromptConsent:
|
||||
return oidc_pb.Prompt_PROMPT_CONSENT
|
||||
case domain.PromptSelectAccount:
|
||||
return oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT
|
||||
case domain.PromptCreate:
|
||||
return oidc_pb.Prompt_PROMPT_CREATE
|
||||
default:
|
||||
return oidc_pb.Prompt_PROMPT_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
switch v := req.GetCallbackKind().(type) {
|
||||
case *oidc_pb.CreateCallbackRequest_Error:
|
||||
return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error)
|
||||
case *oidc_pb.CreateCallbackRequest_Session:
|
||||
return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
|
||||
callback, err := oidc.CreateErrorCallbackURL(authReq, errorReasonToOIDC(ae.GetError()), ae.GetErrorDescription(), ae.GetErrorUri(), s.op.Provider())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oidc_pb.CreateCallbackResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
CallbackUrl: callback,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) {
|
||||
details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authReq := &oidc.AuthRequestV2{CurrentAuthRequest: aar}
|
||||
ctx = op.ContextWithIssuer(ctx, http.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), s.externalSecure))
|
||||
var callback string
|
||||
if aar.ResponseType == domain.OIDCResponseTypeCode {
|
||||
callback, err = oidc.CreateCodeCallbackURL(ctx, authReq, s.op.Provider())
|
||||
} else {
|
||||
callback, err = s.op.CreateTokenCallbackURL(ctx, authReq)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oidc_pb.CreateCallbackResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
CallbackUrl: callback,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func errorReasonToDomain(errorReason oidc_pb.ErrorReason) domain.OIDCErrorReason {
|
||||
switch errorReason {
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED:
|
||||
return domain.OIDCErrorReasonUnspecified
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
|
||||
return domain.OIDCErrorReasonInvalidRequest
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
|
||||
return domain.OIDCErrorReasonUnauthorizedClient
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
|
||||
return domain.OIDCErrorReasonAccessDenied
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
|
||||
return domain.OIDCErrorReasonUnsupportedResponseType
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
|
||||
return domain.OIDCErrorReasonInvalidScope
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
|
||||
return domain.OIDCErrorReasonServerError
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
|
||||
return domain.OIDCErrorReasonTemporaryUnavailable
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
|
||||
return domain.OIDCErrorReasonInteractionRequired
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
|
||||
return domain.OIDCErrorReasonLoginRequired
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
|
||||
return domain.OIDCErrorReasonAccountSelectionRequired
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
|
||||
return domain.OIDCErrorReasonConsentRequired
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
|
||||
return domain.OIDCErrorReasonInvalidRequestURI
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
|
||||
return domain.OIDCErrorReasonInvalidRequestObject
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
|
||||
return domain.OIDCErrorReasonRequestNotSupported
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
|
||||
return domain.OIDCErrorReasonRequestURINotSupported
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
|
||||
return domain.OIDCErrorReasonRegistrationNotSupported
|
||||
default:
|
||||
return domain.OIDCErrorReasonUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
|
||||
switch reason {
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST:
|
||||
return "invalid_request"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT:
|
||||
return "unauthorized_client"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED:
|
||||
return "access_denied"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE:
|
||||
return "unsupported_response_type"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE:
|
||||
return "invalid_scope"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE:
|
||||
return "temporarily_unavailable"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED:
|
||||
return "interaction_required"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED:
|
||||
return "login_required"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED:
|
||||
return "account_selection_required"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED:
|
||||
return "consent_required"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI:
|
||||
return "invalid_request_uri"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT:
|
||||
return "invalid_request_object"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED:
|
||||
return "request_not_supported"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED:
|
||||
return "request_uri_not_supported"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED:
|
||||
return "registration_not_supported"
|
||||
case oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED, oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR:
|
||||
fallthrough
|
||||
default:
|
||||
return "server_error"
|
||||
}
|
||||
}
|
258
internal/api/grpc/oidc/v2beta/oidc_integration_test.go
Normal file
258
internal/api/grpc/oidc/v2beta/oidc_integration_test.go
Normal file
@ -0,0 +1,258 @@
|
||||
//go:build integration
|
||||
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client oidc_pb.OIDCServiceClient
|
||||
User *user.AddHumanUserResponse
|
||||
)
|
||||
|
||||
const (
|
||||
redirectURI = "oidcintegrationtest://callback"
|
||||
redirectURIImplicit = "http://localhost:9999/callback"
|
||||
logoutRedirectURI = "oidcintegrationtest://logged-out"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, _, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
defer Tester.Done()
|
||||
Client = Tester.Client.OIDCv2beta
|
||||
|
||||
CTX = Tester.WithAuthorization(ctx, integration.OrgOwner)
|
||||
User = Tester.CreateHumanUser(CTX)
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func TestServer_GetAuthRequest(t *testing.T) {
|
||||
project, err := Tester.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
|
||||
require.NoError(t, err)
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
AuthRequestID string
|
||||
want *oidc_pb.GetAuthRequestResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
AuthRequestID: "123",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
AuthRequestID: authRequestID,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.GetAuthRequest(CTX, &oidc_pb.GetAuthRequestRequest{
|
||||
AuthRequestId: tt.AuthRequestID,
|
||||
})
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
authRequest := got.GetAuthRequest()
|
||||
assert.NotNil(t, authRequest)
|
||||
assert.Equal(t, authRequestID, authRequest.GetId())
|
||||
assert.WithinRange(t, authRequest.GetCreationDate().AsTime(), now.Add(-time.Second), now.Add(time.Second))
|
||||
assert.Contains(t, authRequest.GetScope(), "openid")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreateCallback(t *testing.T) {
|
||||
project, err := Tester.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false)
|
||||
require.NoError(t, err)
|
||||
sessionResp, err := Tester.Client.SessionV2beta.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *oidc_pb.CreateCallbackRequest
|
||||
AuthError string
|
||||
want *oidc_pb.CreateCallbackResponse
|
||||
wantURL *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Not found",
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: "123",
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session not found",
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: "foo",
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "session token invalid",
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "fail callback",
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Error{
|
||||
Error: &oidc_pb.AuthorizationError{
|
||||
Error: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
|
||||
ErrorDescription: gu.Ptr("nope"),
|
||||
ErrorUri: gu.Ptr("https://example.com/docs"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &oidc_pb.CreateCallbackResponse{
|
||||
CallbackUrl: regexp.QuoteMeta(`oidcintegrationtest://callback?error=access_denied&error_description=nope&error_uri=https%3A%2F%2Fexample.com%2Fdocs&state=state`),
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "code callback",
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequest(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &oidc_pb.CreateCallbackResponse{
|
||||
CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`,
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "implicit",
|
||||
req: &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: func() string {
|
||||
client, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequestImplicit(CTX, client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return authRequestID
|
||||
}(),
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionResp.GetSessionId(),
|
||||
SessionToken: sessionResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &oidc_pb.CreateCallbackResponse{
|
||||
CallbackUrl: `http:\/\/localhost:9999\/callback#access_token=(.*)&expires_in=(.*)&id_token=(.*)&state=state&token_type=Bearer`,
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.CreateCallback(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want != nil {
|
||||
assert.Regexp(t, regexp.MustCompile(tt.want.CallbackUrl), got.GetCallbackUrl())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
150
internal/api/grpc/oidc/v2beta/oidc_test.go
Normal file
150
internal/api/grpc/oidc/v2beta/oidc_test.go
Normal file
@ -0,0 +1,150 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
)
|
||||
|
||||
func Test_authRequestToPb(t *testing.T) {
|
||||
now := time.Now()
|
||||
arg := &query.AuthRequest{
|
||||
ID: "authID",
|
||||
CreationDate: now,
|
||||
ClientID: "clientID",
|
||||
Scope: []string{"a", "b", "c"},
|
||||
RedirectURI: "callbackURI",
|
||||
Prompt: []domain.Prompt{
|
||||
domain.PromptUnspecified,
|
||||
domain.PromptNone,
|
||||
domain.PromptLogin,
|
||||
domain.PromptConsent,
|
||||
domain.PromptSelectAccount,
|
||||
domain.PromptCreate,
|
||||
999,
|
||||
},
|
||||
UiLocales: []string{"en", "fi"},
|
||||
LoginHint: gu.Ptr("foo@bar.com"),
|
||||
MaxAge: gu.Ptr(time.Minute),
|
||||
HintUserID: gu.Ptr("userID"),
|
||||
}
|
||||
want := &oidc_pb.AuthRequest{
|
||||
Id: "authID",
|
||||
CreationDate: timestamppb.New(now),
|
||||
ClientId: "clientID",
|
||||
RedirectUri: "callbackURI",
|
||||
Prompt: []oidc_pb.Prompt{
|
||||
oidc_pb.Prompt_PROMPT_UNSPECIFIED,
|
||||
oidc_pb.Prompt_PROMPT_NONE,
|
||||
oidc_pb.Prompt_PROMPT_LOGIN,
|
||||
oidc_pb.Prompt_PROMPT_CONSENT,
|
||||
oidc_pb.Prompt_PROMPT_SELECT_ACCOUNT,
|
||||
oidc_pb.Prompt_PROMPT_CREATE,
|
||||
oidc_pb.Prompt_PROMPT_UNSPECIFIED,
|
||||
},
|
||||
UiLocales: []string{"en", "fi"},
|
||||
Scope: []string{"a", "b", "c"},
|
||||
LoginHint: gu.Ptr("foo@bar.com"),
|
||||
MaxAge: durationpb.New(time.Minute),
|
||||
HintUserId: gu.Ptr("userID"),
|
||||
}
|
||||
got := authRequestToPb(arg)
|
||||
if !proto.Equal(want, got) {
|
||||
t.Errorf("authRequestToPb() =\n%v\nwant\n%v\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_errorReasonToOIDC(t *testing.T) {
|
||||
tests := []struct {
|
||||
reason oidc_pb.ErrorReason
|
||||
want string
|
||||
}{
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_UNSPECIFIED,
|
||||
want: "server_error",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST,
|
||||
want: "invalid_request",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_UNAUTHORIZED_CLIENT,
|
||||
want: "unauthorized_client",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_ACCESS_DENIED,
|
||||
want: "access_denied",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_UNSUPPORTED_RESPONSE_TYPE,
|
||||
want: "unsupported_response_type",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_SCOPE,
|
||||
want: "invalid_scope",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_SERVER_ERROR,
|
||||
want: "server_error",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_TEMPORARY_UNAVAILABLE,
|
||||
want: "temporarily_unavailable",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_INTERACTION_REQUIRED,
|
||||
want: "interaction_required",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_LOGIN_REQUIRED,
|
||||
want: "login_required",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_ACCOUNT_SELECTION_REQUIRED,
|
||||
want: "account_selection_required",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_CONSENT_REQUIRED,
|
||||
want: "consent_required",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_URI,
|
||||
want: "invalid_request_uri",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_INVALID_REQUEST_OBJECT,
|
||||
want: "invalid_request_object",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_NOT_SUPPORTED,
|
||||
want: "request_not_supported",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_REQUEST_URI_NOT_SUPPORTED,
|
||||
want: "request_uri_not_supported",
|
||||
},
|
||||
{
|
||||
reason: oidc_pb.ErrorReason_ERROR_REASON_REGISTRATION_NOT_SUPPORTED,
|
||||
want: "registration_not_supported",
|
||||
},
|
||||
{
|
||||
reason: 99999,
|
||||
want: "server_error",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.reason.String(), func(t *testing.T) {
|
||||
got := errorReasonToOIDC(tt.reason)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
59
internal/api/grpc/oidc/v2beta/server.go
Normal file
59
internal/api/grpc/oidc/v2beta/server.go
Normal file
@ -0,0 +1,59 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
|
||||
)
|
||||
|
||||
var _ oidc_pb.OIDCServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
oidc_pb.UnimplementedOIDCServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
|
||||
op *oidc.Server
|
||||
externalSecure bool
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
op *oidc.Server,
|
||||
externalSecure bool,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
op: op,
|
||||
externalSecure: externalSecure,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
oidc_pb.RegisterOIDCServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return oidc_pb.OIDCService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return oidc_pb.OIDCService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return oidc_pb.OIDCService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return oidc_pb.RegisterOIDCServiceHandler
|
||||
}
|
@ -7,7 +7,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
)
|
||||
|
||||
func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) {
|
||||
|
@ -14,8 +14,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -12,9 +12,9 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func Test_addOrganizationRequestToCommand(t *testing.T) {
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
)
|
||||
|
||||
var _ org.OrganizationServiceServer = (*Server)(nil)
|
||||
|
83
internal/api/grpc/org/v2beta/org.go
Normal file
83
internal/api/grpc/org/v2beta/org.go
Normal file
@ -0,0 +1,83 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) {
|
||||
orgSetup, err := addOrganizationRequestToCommand(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createdOrg, err := s.command.SetUpOrg(ctx, orgSetup, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createdOrganizationToPb(createdOrg)
|
||||
}
|
||||
|
||||
func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) {
|
||||
admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command.OrgSetup{
|
||||
Name: request.GetName(),
|
||||
CustomDomain: "",
|
||||
Admins: admins,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) {
|
||||
admins = make([]*command.OrgSetupAdmin, len(requestAdmins))
|
||||
for i, admin := range requestAdmins {
|
||||
admins[i], err = addOrganizationRequestAdminToCommand(admin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return admins, nil
|
||||
}
|
||||
|
||||
func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) {
|
||||
switch a := admin.GetUserType().(type) {
|
||||
case *org.AddOrganizationRequest_Admin_UserId:
|
||||
return &command.OrgSetupAdmin{
|
||||
ID: a.UserId,
|
||||
Roles: admin.GetRoles(),
|
||||
}, nil
|
||||
case *org.AddOrganizationRequest_Admin_Human:
|
||||
human, err := user.AddUserRequestToAddHuman(a.Human)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &command.OrgSetupAdmin{
|
||||
Human: human,
|
||||
Roles: admin.GetRoles(),
|
||||
}, nil
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a)
|
||||
}
|
||||
}
|
||||
|
||||
func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) {
|
||||
admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins))
|
||||
for i, admin := range createdOrg.CreatedAdmins {
|
||||
admins[i] = &org.AddOrganizationResponse_CreatedAdmin{
|
||||
UserId: admin.ID,
|
||||
EmailCode: admin.EmailCode,
|
||||
PhoneCode: admin.PhoneCode,
|
||||
}
|
||||
}
|
||||
return &org.AddOrganizationResponse{
|
||||
Details: object.DomainToDetailsPb(createdOrg.ObjectDetails),
|
||||
OrganizationId: createdOrg.ObjectDetails.ResourceOwner,
|
||||
CreatedAdmins: admins,
|
||||
}, nil
|
||||
}
|
207
internal/api/grpc/org/v2beta/org_integration_test.go
Normal file
207
internal/api/grpc/org/v2beta/org_integration_test.go
Normal file
@ -0,0 +1,207 @@
|
||||
//go:build integration
|
||||
|
||||
package org_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client org.OrganizationServiceClient
|
||||
User *user.AddHumanUserResponse
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
defer Tester.Done()
|
||||
Client = Tester.Client.OrgV2beta
|
||||
|
||||
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
|
||||
User = Tester.CreateHumanUser(CTX)
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func TestServer_AddOrganization(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *org.AddOrganizationRequest
|
||||
want *org.AddOrganizationResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing permission",
|
||||
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
|
||||
req: &org.AddOrganizationRequest{
|
||||
Name: "name",
|
||||
Admins: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
ctx: CTX,
|
||||
req: &org.AddOrganizationRequest{
|
||||
Name: "",
|
||||
Admins: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid admin type",
|
||||
ctx: CTX,
|
||||
req: &org.AddOrganizationRequest{
|
||||
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
Admins: []*org.AddOrganizationRequest_Admin{
|
||||
{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "admin with init",
|
||||
ctx: CTX,
|
||||
req: &org.AddOrganizationRequest{
|
||||
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
Admins: []*org.AddOrganizationRequest_Admin{
|
||||
{
|
||||
UserType: &org.AddOrganizationRequest_Admin_Human{
|
||||
Human: &user_v2beta.AddHumanUserRequest{
|
||||
Profile: &user_v2beta.SetHumanProfile{
|
||||
GivenName: "firstname",
|
||||
FamilyName: "lastname",
|
||||
},
|
||||
Email: &user_v2beta.SetHumanEmail{
|
||||
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
|
||||
Verification: &user_v2beta.SetHumanEmail_ReturnCode{
|
||||
ReturnCode: &user_v2beta.ReturnEmailVerificationCode{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &org.AddOrganizationResponse{
|
||||
OrganizationId: integration.NotEmpty,
|
||||
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
|
||||
{
|
||||
UserId: integration.NotEmpty,
|
||||
EmailCode: gu.Ptr(integration.NotEmpty),
|
||||
PhoneCode: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user and new human with idp",
|
||||
ctx: CTX,
|
||||
req: &org.AddOrganizationRequest{
|
||||
Name: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||
Admins: []*org.AddOrganizationRequest_Admin{
|
||||
{
|
||||
UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()},
|
||||
},
|
||||
{
|
||||
UserType: &org.AddOrganizationRequest_Admin_Human{
|
||||
Human: &user_v2beta.AddHumanUserRequest{
|
||||
Profile: &user_v2beta.SetHumanProfile{
|
||||
GivenName: "firstname",
|
||||
FamilyName: "lastname",
|
||||
},
|
||||
Email: &user_v2beta.SetHumanEmail{
|
||||
Email: fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
|
||||
Verification: &user_v2beta.SetHumanEmail_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
IdpLinks: []*user_v2beta.IDPLink{
|
||||
{
|
||||
IdpId: idpID,
|
||||
UserId: "userID",
|
||||
UserName: "username",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &org.AddOrganizationResponse{
|
||||
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
|
||||
// a single admin is expected, because the first provided already exists
|
||||
{
|
||||
UserId: integration.NotEmpty,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddOrganization(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// check details
|
||||
assert.NotZero(t, got.GetDetails().GetSequence())
|
||||
gotCD := got.GetDetails().GetChangeDate().AsTime()
|
||||
now := time.Now()
|
||||
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
|
||||
assert.NotEmpty(t, got.GetDetails().GetResourceOwner())
|
||||
|
||||
// organization id must be the same as the resourceOwner
|
||||
assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId())
|
||||
|
||||
// check the admins
|
||||
require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins()))
|
||||
for i, admin := range tt.want.GetCreatedAdmins() {
|
||||
gotAdmin := got.GetCreatedAdmins()[i]
|
||||
assertCreatedAdmin(t, admin, gotAdmin)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) {
|
||||
if expected.GetUserId() != "" {
|
||||
assert.NotEmpty(t, got.GetUserId())
|
||||
} else {
|
||||
assert.Empty(t, got.GetUserId())
|
||||
}
|
||||
if expected.GetEmailCode() != "" {
|
||||
assert.NotEmpty(t, got.GetEmailCode())
|
||||
} else {
|
||||
assert.Empty(t, got.GetEmailCode())
|
||||
}
|
||||
if expected.GetPhoneCode() != "" {
|
||||
assert.NotEmpty(t, got.GetPhoneCode())
|
||||
} else {
|
||||
assert.Empty(t, got.GetPhoneCode())
|
||||
}
|
||||
}
|
172
internal/api/grpc/org/v2beta/org_test.go
Normal file
172
internal/api/grpc/org/v2beta/org_test.go
Normal file
@ -0,0 +1,172 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func Test_addOrganizationRequestToCommand(t *testing.T) {
|
||||
type args struct {
|
||||
request *org.AddOrganizationRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *command.OrgSetup
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil user",
|
||||
args: args{
|
||||
request: &org.AddOrganizationRequest{
|
||||
Name: "name",
|
||||
Admins: []*org.AddOrganizationRequest_Admin{
|
||||
{},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil),
|
||||
},
|
||||
{
|
||||
name: "user ID",
|
||||
args: args{
|
||||
request: &org.AddOrganizationRequest{
|
||||
Name: "name",
|
||||
Admins: []*org.AddOrganizationRequest_Admin{
|
||||
{
|
||||
UserType: &org.AddOrganizationRequest_Admin_UserId{
|
||||
UserId: "userID",
|
||||
},
|
||||
Roles: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &command.OrgSetup{
|
||||
Name: "name",
|
||||
CustomDomain: "",
|
||||
Admins: []*command.OrgSetupAdmin{
|
||||
{
|
||||
ID: "userID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "human user",
|
||||
args: args{
|
||||
request: &org.AddOrganizationRequest{
|
||||
Name: "name",
|
||||
Admins: []*org.AddOrganizationRequest_Admin{
|
||||
{
|
||||
UserType: &org.AddOrganizationRequest_Admin_Human{
|
||||
Human: &user.AddHumanUserRequest{
|
||||
Profile: &user.SetHumanProfile{
|
||||
GivenName: "firstname",
|
||||
FamilyName: "lastname",
|
||||
},
|
||||
Email: &user.SetHumanEmail{
|
||||
Email: "email@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
Roles: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &command.OrgSetup{
|
||||
Name: "name",
|
||||
CustomDomain: "",
|
||||
Admins: []*command.OrgSetupAdmin{
|
||||
{
|
||||
Human: &command.AddHuman{
|
||||
Username: "email@test.com",
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
Email: command.Email{
|
||||
Address: "email@test.com",
|
||||
},
|
||||
Metadata: make([]*command.AddMetadataEntry, 0),
|
||||
Links: make([]*command.AddLink, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := addOrganizationRequestToCommand(tt.args.request)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createdOrganizationToPb(t *testing.T) {
|
||||
now := time.Now()
|
||||
type args struct {
|
||||
createdOrg *command.CreatedOrg
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *org.AddOrganizationResponse
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "human user with phone and email code",
|
||||
args: args{
|
||||
createdOrg: &command.CreatedOrg{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 1,
|
||||
EventDate: now,
|
||||
ResourceOwner: "orgID",
|
||||
},
|
||||
CreatedAdmins: []*command.CreatedOrgAdmin{
|
||||
{
|
||||
ID: "id",
|
||||
EmailCode: gu.Ptr("emailCode"),
|
||||
PhoneCode: gu.Ptr("phoneCode"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &org.AddOrganizationResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.New(now),
|
||||
ResourceOwner: "orgID",
|
||||
},
|
||||
OrganizationId: "orgID",
|
||||
CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{
|
||||
{
|
||||
UserId: "id",
|
||||
EmailCode: gu.Ptr("emailCode"),
|
||||
PhoneCode: gu.Ptr("phoneCode"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := createdOrganizationToPb(tt.args.createdOrg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
55
internal/api/grpc/org/v2beta/server.go
Normal file
55
internal/api/grpc/org/v2beta/server.go
Normal file
@ -0,0 +1,55 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
)
|
||||
|
||||
var _ org.OrganizationServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
org.UnimplementedOrganizationServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
checkPermission domain.PermissionCheck
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
checkPermission domain.PermissionCheck,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
checkPermission: checkPermission,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
org.RegisterOrganizationServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return org.OrganizationService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return org.OrganizationService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return org.OrganizationService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return org.RegisterOrganizationServiceHandler
|
||||
}
|
@ -29,6 +29,8 @@ func ActivityInterceptor() grpc.UnaryServerInterceptor {
|
||||
var resourcePrefixes = []string{
|
||||
"/zitadel.management.v1.ManagementService/",
|
||||
"/zitadel.admin.v1.AdminService/",
|
||||
"/zitadel.user.v2.UserService/",
|
||||
"/zitadel.settings.v2.SettingsService/",
|
||||
"/zitadel.user.v2beta.UserService/",
|
||||
"/zitadel.settings.v2beta.SettingsService/",
|
||||
"/zitadel.auth.v1.AuthService/",
|
||||
|
@ -131,7 +131,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -153,7 +153,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -181,7 +181,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -211,7 +211,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
@ -240,7 +240,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
@ -264,7 +264,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -293,7 +293,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeAsync,
|
||||
Timeout: time.Second,
|
||||
@ -321,7 +321,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeAsync,
|
||||
Timeout: time.Minute,
|
||||
@ -349,7 +349,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
@ -377,7 +377,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Second,
|
||||
@ -406,7 +406,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
@ -435,7 +435,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target1",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -443,7 +443,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target2",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -451,7 +451,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target3",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -493,7 +493,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target1",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -501,7 +501,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target2",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
@ -509,7 +509,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target3",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
@ -687,7 +687,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "request./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
@ -716,7 +716,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
executionTargets: []execution.Target{
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
ExecutionID: "response./zitadel.session.v2beta.SessionService/SetSession",
|
||||
ExecutionID: "response./zitadel.session.v2.SessionService/SetSession",
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
var _ session.SessionServiceServer = (*Server)(nil)
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -23,9 +23,9 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -860,7 +860,7 @@ func TestServer_SetSession_expired(t *testing.T) {
|
||||
|
||||
// ensure session expires and does not work anymore
|
||||
time.Sleep(20 * time.Second)
|
||||
_, err = Tester.Client.SessionV2.SetSession(CTX, &session.SetSessionRequest{
|
||||
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Lifetime: durationpb.New(20 * time.Second),
|
||||
})
|
||||
@ -944,7 +944,7 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
|
||||
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
}
|
||||
@ -953,7 +953,7 @@ func Test_ZITADEL_API_success(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
|
||||
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
|
||||
@ -966,17 +966,17 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
|
||||
|
||||
// test session token works
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
//terminate the session and test it does not work anymore
|
||||
_, err = Tester.Client.SessionV2.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: id,
|
||||
SessionToken: gu.Ptr(token),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ctx = Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
_, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@ -985,12 +985,12 @@ func Test_ZITADEL_API_session_expired(t *testing.T) {
|
||||
|
||||
// test session token works
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure session expires and does not work anymore
|
||||
time.Sleep(20 * time.Second)
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
51
internal/api/grpc/session/v2beta/server.go
Normal file
51
internal/api/grpc/session/v2beta/server.go
Normal file
@ -0,0 +1,51 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
)
|
||||
|
||||
var _ session.SessionServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
session.UnimplementedSessionServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
session.RegisterSessionServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return session.SessionService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return session.SessionService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return session.SessionService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return session.RegisterSessionServiceHandler
|
||||
}
|
500
internal/api/grpc/session/v2beta/session.go
Normal file
500
internal/api/grpc/session/v2beta/session.go
Normal file
@ -0,0 +1,500 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
timestampComparisons = map[objpb.TimestampQueryMethod]query.TimestampComparison{
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_EQUALS: query.TimestampEquals,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER: query.TimestampGreater,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER_OR_EQUALS: query.TimestampGreaterOrEquals,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS: query.TimestampLess,
|
||||
objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS_OR_EQUALS: query.TimestampLessOrEquals,
|
||||
}
|
||||
)
|
||||
|
||||
func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) {
|
||||
res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session.GetSessionResponse{
|
||||
Session: sessionToPb(res),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) {
|
||||
queries, err := listSessionsRequestToQuery(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessions, err := s.query.SearchSessions(ctx, queries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session.ListSessionsResponse{
|
||||
Details: object.ToListDetails(sessions.SearchResponse),
|
||||
Sessions: sessionsToPb(sessions.Sessions),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) {
|
||||
checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set, err := s.command.CreateSession(ctx, cmds, metadata, userAgent, lifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &session.CreateSessionResponse{
|
||||
Details: object.DomainToDetailsPb(set.ObjectDetails),
|
||||
SessionId: set.ID,
|
||||
SessionToken: set.NewToken,
|
||||
Challenges: challengeResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest) (*session.SetSessionResponse, error) {
|
||||
checks, err := s.setSessionRequestToCommand(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set, err := s.command.UpdateSession(ctx, req.GetSessionId(), cmds, req.GetMetadata(), req.GetLifetime().AsDuration())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session.SetSessionResponse{
|
||||
Details: object.DomainToDetailsPb(set.ObjectDetails),
|
||||
SessionToken: set.NewToken,
|
||||
Challenges: challengeResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteSession(ctx context.Context, req *session.DeleteSessionRequest) (*session.DeleteSessionResponse, error) {
|
||||
details, err := s.command.TerminateSession(ctx, req.GetSessionId(), req.GetSessionToken())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session.DeleteSessionResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sessionsToPb(sessions []*query.Session) []*session.Session {
|
||||
s := make([]*session.Session, len(sessions))
|
||||
for i, session := range sessions {
|
||||
s[i] = sessionToPb(session)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func sessionToPb(s *query.Session) *session.Session {
|
||||
return &session.Session{
|
||||
Id: s.ID,
|
||||
CreationDate: timestamppb.New(s.CreationDate),
|
||||
ChangeDate: timestamppb.New(s.ChangeDate),
|
||||
Sequence: s.Sequence,
|
||||
Factors: factorsToPb(s),
|
||||
Metadata: s.Metadata,
|
||||
UserAgent: userAgentToPb(s.UserAgent),
|
||||
ExpirationDate: expirationToPb(s.Expiration),
|
||||
}
|
||||
}
|
||||
|
||||
func userAgentToPb(ua domain.UserAgent) *session.UserAgent {
|
||||
if ua.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := &session.UserAgent{
|
||||
FingerprintId: ua.FingerprintID,
|
||||
Description: ua.Description,
|
||||
}
|
||||
if ua.IP != nil {
|
||||
out.Ip = gu.Ptr(ua.IP.String())
|
||||
}
|
||||
if ua.Header == nil {
|
||||
return out
|
||||
}
|
||||
out.Header = make(map[string]*session.UserAgent_HeaderValues, len(ua.Header))
|
||||
for k, v := range ua.Header {
|
||||
out.Header[k] = &session.UserAgent_HeaderValues{
|
||||
Values: v,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func expirationToPb(expiration time.Time) *timestamppb.Timestamp {
|
||||
if expiration.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return timestamppb.New(expiration)
|
||||
}
|
||||
|
||||
func factorsToPb(s *query.Session) *session.Factors {
|
||||
user := userFactorToPb(s.UserFactor)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return &session.Factors{
|
||||
User: user,
|
||||
Password: passwordFactorToPb(s.PasswordFactor),
|
||||
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
|
||||
Intent: intentFactorToPb(s.IntentFactor),
|
||||
Totp: totpFactorToPb(s.TOTPFactor),
|
||||
OtpSms: otpFactorToPb(s.OTPSMSFactor),
|
||||
OtpEmail: otpFactorToPb(s.OTPEmailFactor),
|
||||
}
|
||||
}
|
||||
|
||||
func passwordFactorToPb(factor query.SessionPasswordFactor) *session.PasswordFactor {
|
||||
if factor.PasswordCheckedAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &session.PasswordFactor{
|
||||
VerifiedAt: timestamppb.New(factor.PasswordCheckedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func intentFactorToPb(factor query.SessionIntentFactor) *session.IntentFactor {
|
||||
if factor.IntentCheckedAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &session.IntentFactor{
|
||||
VerifiedAt: timestamppb.New(factor.IntentCheckedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func webAuthNFactorToPb(factor query.SessionWebAuthNFactor) *session.WebAuthNFactor {
|
||||
if factor.WebAuthNCheckedAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &session.WebAuthNFactor{
|
||||
VerifiedAt: timestamppb.New(factor.WebAuthNCheckedAt),
|
||||
UserVerified: factor.UserVerified,
|
||||
}
|
||||
}
|
||||
|
||||
func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor {
|
||||
if factor.TOTPCheckedAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &session.TOTPFactor{
|
||||
VerifiedAt: timestamppb.New(factor.TOTPCheckedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func otpFactorToPb(factor query.SessionOTPFactor) *session.OTPFactor {
|
||||
if factor.OTPCheckedAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &session.OTPFactor{
|
||||
VerifiedAt: timestamppb.New(factor.OTPCheckedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
|
||||
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
|
||||
Id: factor.UserID,
|
||||
LoginName: factor.LoginName,
|
||||
DisplayName: factor.DisplayName,
|
||||
OrganizationId: factor.ResourceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
func listSessionsRequestToQuery(ctx context.Context, req *session.ListSessionsRequest) (*query.SessionsSearchQueries, error) {
|
||||
offset, limit, asc := object.ListQueryToQuery(req.Query)
|
||||
queries, err := sessionQueriesToQuery(ctx, req.GetQueries())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &query.SessionsSearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
SortingColumn: fieldNameToSessionColumn(req.GetSortingColumn()),
|
||||
},
|
||||
Queries: queries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sessionQueriesToQuery(ctx context.Context, queries []*session.SearchQuery) (_ []query.SearchQuery, err error) {
|
||||
q := make([]query.SearchQuery, len(queries)+1)
|
||||
for i, v := range queries {
|
||||
q[i], err = sessionQueryToQuery(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
creatorQuery, err := query.NewSessionCreatorSearchQuery(authz.GetCtxData(ctx).UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q[len(queries)] = creatorQuery
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func sessionQueryToQuery(sq *session.SearchQuery) (query.SearchQuery, error) {
|
||||
switch q := sq.Query.(type) {
|
||||
case *session.SearchQuery_IdsQuery:
|
||||
return idsQueryToQuery(q.IdsQuery)
|
||||
case *session.SearchQuery_UserIdQuery:
|
||||
return query.NewUserIDSearchQuery(q.UserIdQuery.GetId())
|
||||
case *session.SearchQuery_CreationDateQuery:
|
||||
return creationDateQueryToQuery(q.CreationDateQuery)
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) {
|
||||
return query.NewSessionIDsSearchQuery(q.Ids)
|
||||
}
|
||||
|
||||
func creationDateQueryToQuery(q *session.CreationDateQuery) (query.SearchQuery, error) {
|
||||
comparison := timestampComparisons[q.GetMethod()]
|
||||
return query.NewCreationDateQuery(q.GetCreationDate().AsTime(), comparison)
|
||||
}
|
||||
|
||||
func fieldNameToSessionColumn(field session.SessionFieldName) query.Column {
|
||||
switch field {
|
||||
case session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE:
|
||||
return query.SessionColumnCreationDate
|
||||
case session.SessionFieldName_SESSION_FIELD_NAME_UNSPECIFIED:
|
||||
// Handle all remaining cases so the linter succeeds
|
||||
return query.Column{}
|
||||
default:
|
||||
return query.Column{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, time.Duration, error) {
|
||||
checks, err := s.checksToCommand(ctx, req.Checks)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
return checks, req.GetMetadata(), userAgentToCommand(req.GetUserAgent()), req.GetLifetime().AsDuration(), nil
|
||||
}
|
||||
|
||||
func userAgentToCommand(userAgent *session.UserAgent) *domain.UserAgent {
|
||||
if userAgent == nil {
|
||||
return nil
|
||||
}
|
||||
out := &domain.UserAgent{
|
||||
FingerprintID: userAgent.FingerprintId,
|
||||
IP: net.ParseIP(userAgent.GetIp()),
|
||||
Description: userAgent.Description,
|
||||
}
|
||||
if len(userAgent.Header) > 0 {
|
||||
out.Header = make(http.Header, len(userAgent.Header))
|
||||
for k, values := range userAgent.Header {
|
||||
out.Header[k] = values.GetValues()
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Server) setSessionRequestToCommand(ctx context.Context, req *session.SetSessionRequest) ([]command.SessionCommand, error) {
|
||||
checks, err := s.checksToCommand(ctx, req.Checks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return checks, nil
|
||||
}
|
||||
|
||||
func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([]command.SessionCommand, error) {
|
||||
checkUser, err := userCheck(checks.GetUser())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionChecks := make([]command.SessionCommand, 0, 7)
|
||||
if checkUser != nil {
|
||||
user, err := checkUser.search(ctx, s.query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !user.State.IsEnabled() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "SESSION-Gj4ko", "Errors.User.NotActive")
|
||||
}
|
||||
|
||||
var preferredLanguage *language.Tag
|
||||
if user.Human != nil && !user.Human.PreferredLanguage.IsRoot() {
|
||||
preferredLanguage = &user.Human.PreferredLanguage
|
||||
}
|
||||
sessionChecks = append(sessionChecks, command.CheckUser(user.ID, user.ResourceOwner, preferredLanguage))
|
||||
}
|
||||
if password := checks.GetPassword(); password != nil {
|
||||
sessionChecks = append(sessionChecks, command.CheckPassword(password.GetPassword()))
|
||||
}
|
||||
if intent := checks.GetIdpIntent(); intent != nil {
|
||||
sessionChecks = append(sessionChecks, command.CheckIntent(intent.GetIdpIntentId(), intent.GetIdpIntentToken()))
|
||||
}
|
||||
if passkey := checks.GetWebAuthN(); passkey != nil {
|
||||
sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData()))
|
||||
}
|
||||
if totp := checks.GetTotp(); totp != nil {
|
||||
sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetCode()))
|
||||
}
|
||||
if otp := checks.GetOtpSms(); otp != nil {
|
||||
sessionChecks = append(sessionChecks, command.CheckOTPSMS(otp.GetCode()))
|
||||
}
|
||||
if otp := checks.GetOtpEmail(); otp != nil {
|
||||
sessionChecks = append(sessionChecks, command.CheckOTPEmail(otp.GetCode()))
|
||||
}
|
||||
return sessionChecks, nil
|
||||
}
|
||||
|
||||
func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand, error) {
|
||||
if challenges == nil {
|
||||
return nil, cmds, nil
|
||||
}
|
||||
resp := new(session.Challenges)
|
||||
if req := challenges.GetWebAuthN(); req != nil {
|
||||
challenge, cmd := s.createWebAuthNChallengeCommand(req)
|
||||
resp.WebAuthN = challenge
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
if req := challenges.GetOtpSms(); req != nil {
|
||||
challenge, cmd := s.createOTPSMSChallengeCommand(req)
|
||||
resp.OtpSms = challenge
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
if req := challenges.GetOtpEmail(); req != nil {
|
||||
challenge, cmd, err := s.createOTPEmailChallengeCommand(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
resp.OtpEmail = challenge
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return resp, cmds, nil
|
||||
}
|
||||
|
||||
func (s *Server) createWebAuthNChallengeCommand(req *session.RequestChallenges_WebAuthN) (*session.Challenges_WebAuthN, command.SessionCommand) {
|
||||
challenge := &session.Challenges_WebAuthN{
|
||||
PublicKeyCredentialRequestOptions: new(structpb.Struct),
|
||||
}
|
||||
userVerification := userVerificationRequirementToDomain(req.GetUserVerificationRequirement())
|
||||
return challenge, s.command.CreateWebAuthNChallenge(userVerification, req.GetDomain(), challenge.PublicKeyCredentialRequestOptions)
|
||||
}
|
||||
|
||||
func userVerificationRequirementToDomain(req session.UserVerificationRequirement) domain.UserVerificationRequirement {
|
||||
switch req {
|
||||
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED:
|
||||
return domain.UserVerificationRequirementUnspecified
|
||||
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED:
|
||||
return domain.UserVerificationRequirementRequired
|
||||
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED:
|
||||
return domain.UserVerificationRequirementPreferred
|
||||
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED:
|
||||
return domain.UserVerificationRequirementDiscouraged
|
||||
default:
|
||||
return domain.UserVerificationRequirementUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) createOTPSMSChallengeCommand(req *session.RequestChallenges_OTPSMS) (*string, command.SessionCommand) {
|
||||
if req.GetReturnCode() {
|
||||
challenge := new(string)
|
||||
return challenge, s.command.CreateOTPSMSChallengeReturnCode(challenge)
|
||||
}
|
||||
|
||||
return nil, s.command.CreateOTPSMSChallenge()
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) createOTPEmailChallengeCommand(req *session.RequestChallenges_OTPEmail) (*string, command.SessionCommand, error) {
|
||||
switch t := req.GetDeliveryType().(type) {
|
||||
case *session.RequestChallenges_OTPEmail_SendCode_:
|
||||
cmd, err := s.command.CreateOTPEmailChallengeURLTemplate(t.SendCode.GetUrlTemplate())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, cmd, nil
|
||||
case *session.RequestChallenges_OTPEmail_ReturnCode_:
|
||||
challenge := new(string)
|
||||
return challenge, s.command.CreateOTPEmailChallengeReturnCode(challenge), nil
|
||||
case nil:
|
||||
return nil, s.command.CreateOTPEmailChallenge(), nil
|
||||
default:
|
||||
return nil, nil, zerrors.ThrowUnimplementedf(nil, "SESSION-k3ng0", "delivery_type oneOf %T in OTPEmailChallenge not implemented", t)
|
||||
}
|
||||
}
|
||||
|
||||
func userCheck(user *session.CheckUser) (userSearch, error) {
|
||||
if user == nil {
|
||||
return nil, nil
|
||||
}
|
||||
switch s := user.GetSearch().(type) {
|
||||
case *session.CheckUser_UserId:
|
||||
return userByID(s.UserId), nil
|
||||
case *session.CheckUser_LoginName:
|
||||
return userByLoginName(s.LoginName)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", s)
|
||||
}
|
||||
}
|
||||
|
||||
type userSearch interface {
|
||||
search(ctx context.Context, q *query.Queries) (*query.User, error)
|
||||
}
|
||||
|
||||
func userByID(userID string) userSearch {
|
||||
return userSearchByID{userID}
|
||||
}
|
||||
|
||||
func userByLoginName(loginName string) (userSearch, error) {
|
||||
return userSearchByLoginName{loginName}, nil
|
||||
}
|
||||
|
||||
type userSearchByID struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.User, error) {
|
||||
return q.GetUserByID(ctx, true, u.id)
|
||||
}
|
||||
|
||||
type userSearchByLoginName struct {
|
||||
loginName string
|
||||
}
|
||||
|
||||
func (u userSearchByLoginName) search(ctx context.Context, q *query.Queries) (*query.User, error) {
|
||||
return q.GetUserByLoginName(ctx, true, u.loginName)
|
||||
}
|
996
internal/api/grpc/session/v2beta/session_integration_test.go
Normal file
996
internal/api/grpc/session/v2beta/session_integration_test.go
Normal file
@ -0,0 +1,996 @@
|
||||
//go:build integration
|
||||
|
||||
package session_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/logging"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
IAMOwnerCTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client session.SessionServiceClient
|
||||
User *user.AddHumanUserResponse
|
||||
DeactivatedUser *user.AddHumanUserResponse
|
||||
LockedUser *user.AddHumanUserResponse
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
defer Tester.Done()
|
||||
Client = Tester.Client.SessionV2beta
|
||||
|
||||
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
|
||||
IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
|
||||
User = createFullUser(CTX)
|
||||
DeactivatedUser = createDeactivatedUser(CTX)
|
||||
LockedUser = createLockedUser(CTX)
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func createFullUser(ctx context.Context) *user.AddHumanUserResponse {
|
||||
userResp := Tester.CreateHumanUser(ctx)
|
||||
Tester.Client.UserV2.VerifyEmail(ctx, &user.VerifyEmailRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
VerificationCode: userResp.GetEmailCode(),
|
||||
})
|
||||
Tester.Client.UserV2.VerifyPhone(ctx, &user.VerifyPhoneRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
VerificationCode: userResp.GetPhoneCode(),
|
||||
})
|
||||
Tester.SetUserPassword(ctx, userResp.GetUserId(), integration.UserPassword, false)
|
||||
Tester.RegisterUserPasskey(ctx, userResp.GetUserId())
|
||||
return userResp
|
||||
}
|
||||
|
||||
func createDeactivatedUser(ctx context.Context) *user.AddHumanUserResponse {
|
||||
userResp := Tester.CreateHumanUser(ctx)
|
||||
_, err := Tester.Client.UserV2.DeactivateUser(ctx, &user.DeactivateUserRequest{UserId: userResp.GetUserId()})
|
||||
logging.OnError(err).Fatal("deactivate human user")
|
||||
return userResp
|
||||
}
|
||||
|
||||
func createLockedUser(ctx context.Context) *user.AddHumanUserResponse {
|
||||
userResp := Tester.CreateHumanUser(ctx)
|
||||
_, err := Tester.Client.UserV2.LockUser(ctx, &user.LockUserRequest{UserId: userResp.GetUserId()})
|
||||
logging.OnError(err).Fatal("lock human user")
|
||||
return userResp
|
||||
}
|
||||
|
||||
func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, userID string, factors ...wantFactor) *session.Session {
|
||||
t.Helper()
|
||||
require.NotEmpty(t, id)
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
resp, err := Client.GetSession(CTX, &session.GetSessionRequest{
|
||||
SessionId: id,
|
||||
SessionToken: &token,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
s := resp.GetSession()
|
||||
|
||||
assert.Equal(t, id, s.GetId())
|
||||
assert.WithinRange(t, s.GetCreationDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
assert.WithinRange(t, s.GetChangeDate().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
assert.Equal(t, sequence, s.GetSequence())
|
||||
assert.Equal(t, metadata, s.GetMetadata())
|
||||
|
||||
if !proto.Equal(userAgent, s.GetUserAgent()) {
|
||||
t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent)
|
||||
}
|
||||
if expirationWindow == 0 {
|
||||
assert.Nil(t, s.GetExpirationDate())
|
||||
} else {
|
||||
assert.WithinRange(t, s.GetExpirationDate().AsTime(), time.Now().Add(-expirationWindow), time.Now().Add(expirationWindow))
|
||||
}
|
||||
|
||||
verifyFactors(t, s.GetFactors(), window, userID, factors)
|
||||
return s
|
||||
}
|
||||
|
||||
type wantFactor int
|
||||
|
||||
const (
|
||||
wantUserFactor wantFactor = iota
|
||||
wantPasswordFactor
|
||||
wantWebAuthNFactor
|
||||
wantWebAuthNFactorUserVerified
|
||||
wantTOTPFactor
|
||||
wantIntentFactor
|
||||
wantOTPSMSFactor
|
||||
wantOTPEmailFactor
|
||||
)
|
||||
|
||||
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, userID string, want []wantFactor) {
|
||||
for _, w := range want {
|
||||
switch w {
|
||||
case wantUserFactor:
|
||||
uf := factors.GetUser()
|
||||
assert.NotNil(t, uf)
|
||||
assert.WithinRange(t, uf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
assert.Equal(t, userID, uf.GetId())
|
||||
case wantPasswordFactor:
|
||||
pf := factors.GetPassword()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
case wantWebAuthNFactor:
|
||||
pf := factors.GetWebAuthN()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
assert.False(t, pf.GetUserVerified())
|
||||
case wantWebAuthNFactorUserVerified:
|
||||
pf := factors.GetWebAuthN()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
assert.True(t, pf.GetUserVerified())
|
||||
case wantTOTPFactor:
|
||||
pf := factors.GetTotp()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
case wantIntentFactor:
|
||||
pf := factors.GetIntent()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
case wantOTPSMSFactor:
|
||||
pf := factors.GetOtpSms()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
case wantOTPEmailFactor:
|
||||
pf := factors.GetOtpEmail()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreateSession(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req *session.CreateSessionRequest
|
||||
want *session.CreateSessionResponse
|
||||
wantErr bool
|
||||
wantFactors []wantFactor
|
||||
wantUserAgent *session.UserAgent
|
||||
wantExpirationWindow time.Duration
|
||||
}{
|
||||
{
|
||||
name: "empty session",
|
||||
req: &session.CreateSessionRequest{
|
||||
Metadata: map[string][]byte{"foo": []byte("bar")},
|
||||
},
|
||||
want: &session.CreateSessionResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user agent",
|
||||
req: &session.CreateSessionRequest{
|
||||
Metadata: map[string][]byte{"foo": []byte("bar")},
|
||||
UserAgent: &session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fingerPrintID"),
|
||||
Ip: gu.Ptr("1.2.3.4"),
|
||||
Description: gu.Ptr("Description"),
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"foo": {Values: []string{"foo", "bar"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &session.CreateSessionResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
wantUserAgent: &session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fingerPrintID"),
|
||||
Ip: gu.Ptr("1.2.3.4"),
|
||||
Description: gu.Ptr("Description"),
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"foo": {Values: []string{"foo", "bar"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative lifetime",
|
||||
req: &session.CreateSessionRequest{
|
||||
Metadata: map[string][]byte{"foo": []byte("bar")},
|
||||
Lifetime: durationpb.New(-5 * time.Minute),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "lifetime",
|
||||
req: &session.CreateSessionRequest{
|
||||
Metadata: map[string][]byte{"foo": []byte("bar")},
|
||||
Lifetime: durationpb.New(5 * time.Minute),
|
||||
},
|
||||
want: &session.CreateSessionResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
wantExpirationWindow: 5 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "with user",
|
||||
req: &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Metadata: map[string][]byte{"foo": []byte("bar")},
|
||||
},
|
||||
want: &session.CreateSessionResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
wantFactors: []wantFactor{wantUserFactor},
|
||||
},
|
||||
{
|
||||
name: "deactivated user",
|
||||
req: &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: DeactivatedUser.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "locked user",
|
||||
req: &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: LockedUser.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "password without user error",
|
||||
req: &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
Password: &session.CheckPassword{
|
||||
Password: "Difficult",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "passkey without user error",
|
||||
req: &session.CreateSessionRequest{
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
Domain: Tester.Config.ExternalDomain,
|
||||
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "passkey without domain (not registered) error",
|
||||
req: &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.CreateSession(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
|
||||
verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantExpirationWindow, User.GetUserId(), tt.wantFactors...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_lock_user(t *testing.T) {
|
||||
// create a separate org so we don't interfere with any other test
|
||||
org := Tester.CreateOrganization(IAMOwnerCTX,
|
||||
fmt.Sprintf("TestServer_CreateSession_lock_user_%d", time.Now().UnixNano()),
|
||||
fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()),
|
||||
)
|
||||
userID := org.CreatedAdmins[0].GetUserId()
|
||||
Tester.SetUserPassword(IAMOwnerCTX, userID, integration.UserPassword, false)
|
||||
|
||||
// enable password lockout
|
||||
maxAttempts := 2
|
||||
ctxOrg := metadata.AppendToOutgoingContext(IAMOwnerCTX, "x-zitadel-orgid", org.GetOrganizationId())
|
||||
_, err := Tester.Client.Mgmt.AddCustomLockoutPolicy(ctxOrg, &mgmt.AddCustomLockoutPolicyRequest{
|
||||
MaxPasswordAttempts: uint32(maxAttempts),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i <= maxAttempts; i++ {
|
||||
_, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
Password: &session.CheckPassword{
|
||||
Password: "invalid",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
statusCode := status.Code(err)
|
||||
expectedCode := codes.InvalidArgument
|
||||
// as soon as we hit the limit the user is locked and following request will
|
||||
// already deny any check with a precondition failed since the user is locked
|
||||
if i >= maxAttempts {
|
||||
expectedCode = codes.FailedPrecondition
|
||||
}
|
||||
assert.Equal(t, expectedCode, statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_webauthn(t *testing.T) {
|
||||
// create new session with user and request the webauthn challenge
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
Domain: Tester.Config.ExternalDomain,
|
||||
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// update the session with webauthn assertion data
|
||||
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
WebAuthN: &session.CheckWebAuthN{
|
||||
CredentialAssertionData: assertionData,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
|
||||
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentToken: token,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
|
||||
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id")
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentToken: token,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
|
||||
// successful intent without known / linked user
|
||||
idpUserID := "id"
|
||||
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID)
|
||||
|
||||
// link the user (with info from intent)
|
||||
Tester.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
|
||||
|
||||
// session with intent check must now succeed
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentToken: token,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
intentID := Tester.CreateIntent(t, CTX, idpID)
|
||||
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
IdpIntent: &session.CheckIDPIntent{
|
||||
IdpIntentId: intentID,
|
||||
IdpIntentToken: "false",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
|
||||
resp, err := Tester.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
secret = resp.GetSecret()
|
||||
code, err := totp.GenerateCode(secret, time.Now())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Tester.Client.UserV2.VerifyTOTPRegistration(ctx, &user.VerifyTOTPRegistrationRequest{
|
||||
UserId: userID,
|
||||
Code: code,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return secret
|
||||
}
|
||||
|
||||
func registerOTPSMS(ctx context.Context, t *testing.T, userID string) {
|
||||
_, err := Tester.Client.UserV2.AddOTPSMS(ctx, &user.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func registerOTPEmail(ctx context.Context, t *testing.T, userID string) {
|
||||
_, err := Tester.Client.UserV2.AddOTPEmail(ctx, &user.AddOTPEmailRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestServer_SetSession_flow_totp(t *testing.T) {
|
||||
userExisting := createFullUser(CTX)
|
||||
|
||||
// create new, empty session
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
||||
require.NoError(t, err)
|
||||
sessionToken := createResp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, "")
|
||||
|
||||
t.Run("check user", func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: userExisting.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor)
|
||||
})
|
||||
|
||||
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
Domain: Tester.Config.ExternalDomain,
|
||||
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId())
|
||||
sessionToken = resp.GetSessionToken()
|
||||
|
||||
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
WebAuthN: &session.CheckWebAuthN{
|
||||
CredentialAssertionData: assertionData,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
|
||||
})
|
||||
|
||||
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
Tester.RegisterUserU2F(userAuthCtx, userExisting.GetUserId())
|
||||
totpSecret := registerTOTP(userAuthCtx, t, userExisting.GetUserId())
|
||||
registerOTPSMS(userAuthCtx, t, userExisting.GetUserId())
|
||||
registerOTPEmail(userAuthCtx, t, userExisting.GetUserId())
|
||||
|
||||
t.Run("check TOTP", func(t *testing.T) {
|
||||
code, err := totp.GenerateCode(totpSecret, time.Now())
|
||||
require.NoError(t, err)
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
Totp: &session.CheckTOTP{
|
||||
Code: code,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userExisting.GetUserId(), wantUserFactor, wantTOTPFactor)
|
||||
})
|
||||
|
||||
userImport := Tester.CreateHumanUserWithTOTP(CTX, totpSecret)
|
||||
createRespImport, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
||||
require.NoError(t, err)
|
||||
sessionTokenImport := createRespImport.GetSessionToken()
|
||||
verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, createRespImport.GetDetails().GetSequence(), time.Minute, nil, nil, 0, "")
|
||||
|
||||
t.Run("check user", func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createRespImport.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: userImport.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionTokenImport = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userImport.GetUserId(), wantUserFactor)
|
||||
})
|
||||
t.Run("check TOTP", func(t *testing.T) {
|
||||
code, err := totp.GenerateCode(totpSecret, time.Now())
|
||||
require.NoError(t, err)
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createRespImport.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
Totp: &session.CheckTOTP{
|
||||
Code: code,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionTokenImport = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createRespImport.GetSessionId(), sessionTokenImport, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, userImport.GetUserId(), wantUserFactor, wantTOTPFactor)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_SetSession_flow(t *testing.T) {
|
||||
// create new, empty session
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
||||
require.NoError(t, err)
|
||||
sessionToken := createResp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
|
||||
t.Run("check user", func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor)
|
||||
})
|
||||
|
||||
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
Domain: Tester.Config.ExternalDomain,
|
||||
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
sessionToken = resp.GetSessionToken()
|
||||
|
||||
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
WebAuthN: &session.CheckWebAuthN{
|
||||
CredentialAssertionData: assertionData,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactorUserVerified)
|
||||
})
|
||||
|
||||
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
Tester.RegisterUserU2F(userAuthCtx, User.GetUserId())
|
||||
totpSecret := registerTOTP(userAuthCtx, t, User.GetUserId())
|
||||
registerOTPSMS(userAuthCtx, t, User.GetUserId())
|
||||
registerOTPEmail(userAuthCtx, t, User.GetUserId())
|
||||
|
||||
t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) {
|
||||
|
||||
for _, userVerificationRequirement := range []session.UserVerificationRequirement{
|
||||
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
||||
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
|
||||
} {
|
||||
t.Run(userVerificationRequirement.String(), func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Challenges: &session.RequestChallenges{
|
||||
WebAuthN: &session.RequestChallenges_WebAuthN{
|
||||
Domain: Tester.Config.ExternalDomain,
|
||||
UserVerificationRequirement: userVerificationRequirement,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
sessionToken = resp.GetSessionToken()
|
||||
|
||||
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
WebAuthN: &session.CheckWebAuthN{
|
||||
CredentialAssertionData: assertionData,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("check TOTP", func(t *testing.T) {
|
||||
code, err := totp.GenerateCode(totpSecret, time.Now())
|
||||
require.NoError(t, err)
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
Totp: &session.CheckTOTP{
|
||||
Code: code,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantTOTPFactor)
|
||||
})
|
||||
|
||||
t.Run("check OTP SMS", func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Challenges: &session.RequestChallenges{
|
||||
OtpSms: &session.RequestChallenges_OTPSMS{ReturnCode: true},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
sessionToken = resp.GetSessionToken()
|
||||
|
||||
otp := resp.GetChallenges().GetOtpSms()
|
||||
require.NotEmpty(t, otp)
|
||||
|
||||
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
OtpSms: &session.CheckOTP{
|
||||
Code: otp,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor)
|
||||
})
|
||||
|
||||
t.Run("check OTP Email", func(t *testing.T) {
|
||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Challenges: &session.RequestChallenges{
|
||||
OtpEmail: &session.RequestChallenges_OTPEmail{
|
||||
DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
|
||||
sessionToken = resp.GetSessionToken()
|
||||
|
||||
otp := resp.GetChallenges().GetOtpEmail()
|
||||
require.NotEmpty(t, otp)
|
||||
|
||||
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Checks: &session.Checks{
|
||||
OtpEmail: &session.CheckOTP{
|
||||
Code: otp,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sessionToken = resp.GetSessionToken()
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_SetSession_expired(t *testing.T) {
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Lifetime: durationpb.New(20 * time.Second),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// test session token works
|
||||
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Lifetime: durationpb.New(20 * time.Second),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure session expires and does not work anymore
|
||||
time.Sleep(20 * time.Second)
|
||||
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
Lifetime: durationpb.New(20 * time.Second),
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestServer_DeleteSession_token(t *testing.T) {
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr("invalid"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr(createResp.GetSessionToken()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestServer_DeleteSession_own_session(t *testing.T) {
|
||||
// create two users for the test and a session each to get tokens for authorization
|
||||
user1 := Tester.CreateHumanUser(CTX)
|
||||
Tester.SetUserPassword(CTX, user1.GetUserId(), integration.UserPassword, false)
|
||||
_, token1, _, _ := Tester.CreatePasswordSession(t, CTX, user1.GetUserId(), integration.UserPassword)
|
||||
|
||||
user2 := Tester.CreateHumanUser(CTX)
|
||||
Tester.SetUserPassword(CTX, user2.GetUserId(), integration.UserPassword, false)
|
||||
_, token2, _, _ := Tester.CreatePasswordSession(t, CTX, user2.GetUserId(), integration.UserPassword)
|
||||
|
||||
// create a new session for the first user
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: user1.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// delete the new (user1) session must not be possible with user (has no permission)
|
||||
_, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token2), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// delete the new (user1) session by themselves
|
||||
_, err = Client.DeleteSession(Tester.WithAuthorizationToken(context.Background(), token1), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestServer_DeleteSession_with_permission(t *testing.T) {
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// delete the new session by ORG_OWNER
|
||||
_, err = Client.DeleteSession(Tester.WithAuthorization(context.Background(), integration.OrgOwner), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
||||
// create new, empty session
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
|
||||
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_success(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
|
||||
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
|
||||
require.NotNil(t, id, webAuthN.GetVerifiedAt().AsTime())
|
||||
require.True(t, webAuthN.GetUserVerified())
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_session_not_found(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
|
||||
|
||||
// test session token works
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
//terminate the session and test it does not work anymore
|
||||
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: id,
|
||||
SessionToken: gu.Ptr(token),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ctx = Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err = Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_session_expired(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreateVerifiedWebAuthNSessionWithLifetime(t, CTX, User.GetUserId(), 20*time.Second)
|
||||
|
||||
// test session token works
|
||||
ctx := Tester.WithAuthorizationToken(context.Background(), token)
|
||||
_, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure session expires and does not work anymore
|
||||
time.Sleep(20 * time.Second)
|
||||
sessionResp, err := Client.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
}
|
739
internal/api/grpc/session/v2beta/session_test.go
Normal file
739
internal/api/grpc/session/v2beta/session_test.go
Normal file
@ -0,0 +1,739 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
objpb "github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
creationDate = time.Date(2023, 10, 10, 14, 15, 0, 0, time.UTC)
|
||||
)
|
||||
|
||||
func Test_sessionsToPb(t *testing.T) {
|
||||
now := time.Now()
|
||||
past := now.Add(-time.Hour)
|
||||
|
||||
sessions := []*query.Session{
|
||||
{ // no factor, with user agent and expiration
|
||||
ID: "999",
|
||||
CreationDate: now,
|
||||
ChangeDate: now,
|
||||
Sequence: 123,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "me",
|
||||
Creator: "he",
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
UserAgent: domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fingerprintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
IP: net.IPv4(1, 2, 3, 4),
|
||||
Header: http.Header{"foo": []string{"foo", "bar"}},
|
||||
},
|
||||
Expiration: now,
|
||||
},
|
||||
{ // user factor
|
||||
ID: "999",
|
||||
CreationDate: now,
|
||||
ChangeDate: now,
|
||||
Sequence: 123,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "me",
|
||||
Creator: "he",
|
||||
UserFactor: query.SessionUserFactor{
|
||||
UserID: "345",
|
||||
UserCheckedAt: past,
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
{ // password factor
|
||||
ID: "999",
|
||||
CreationDate: now,
|
||||
ChangeDate: now,
|
||||
Sequence: 123,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "me",
|
||||
Creator: "he",
|
||||
UserFactor: query.SessionUserFactor{
|
||||
UserID: "345",
|
||||
UserCheckedAt: past,
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
PasswordFactor: query.SessionPasswordFactor{
|
||||
PasswordCheckedAt: past,
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
{ // webAuthN factor
|
||||
ID: "999",
|
||||
CreationDate: now,
|
||||
ChangeDate: now,
|
||||
Sequence: 123,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "me",
|
||||
Creator: "he",
|
||||
UserFactor: query.SessionUserFactor{
|
||||
UserID: "345",
|
||||
UserCheckedAt: past,
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
WebAuthNFactor: query.SessionWebAuthNFactor{
|
||||
WebAuthNCheckedAt: past,
|
||||
UserVerified: true,
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
{ // totp factor
|
||||
ID: "999",
|
||||
CreationDate: now,
|
||||
ChangeDate: now,
|
||||
Sequence: 123,
|
||||
State: domain.SessionStateActive,
|
||||
ResourceOwner: "me",
|
||||
Creator: "he",
|
||||
UserFactor: query.SessionUserFactor{
|
||||
UserID: "345",
|
||||
UserCheckedAt: past,
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
TOTPFactor: query.SessionTOTPFactor{
|
||||
TOTPCheckedAt: past,
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
}
|
||||
|
||||
want := []*session.Session{
|
||||
{ // no factor, with user agent and expiration
|
||||
Id: "999",
|
||||
CreationDate: timestamppb.New(now),
|
||||
ChangeDate: timestamppb.New(now),
|
||||
Sequence: 123,
|
||||
Factors: nil,
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
UserAgent: &session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fingerprintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
Ip: gu.Ptr("1.2.3.4"),
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"foo": {Values: []string{"foo", "bar"}},
|
||||
},
|
||||
},
|
||||
ExpirationDate: timestamppb.New(now),
|
||||
},
|
||||
{ // user factor
|
||||
Id: "999",
|
||||
CreationDate: timestamppb.New(now),
|
||||
ChangeDate: timestamppb.New(now),
|
||||
Sequence: 123,
|
||||
Factors: &session.Factors{
|
||||
User: &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
OrganizationId: "org1",
|
||||
},
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
{ // password factor
|
||||
Id: "999",
|
||||
CreationDate: timestamppb.New(now),
|
||||
ChangeDate: timestamppb.New(now),
|
||||
Sequence: 123,
|
||||
Factors: &session.Factors{
|
||||
User: &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
OrganizationId: "org1",
|
||||
},
|
||||
Password: &session.PasswordFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
},
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
{ // webAuthN factor
|
||||
Id: "999",
|
||||
CreationDate: timestamppb.New(now),
|
||||
ChangeDate: timestamppb.New(now),
|
||||
Sequence: 123,
|
||||
Factors: &session.Factors{
|
||||
User: &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
OrganizationId: "org1",
|
||||
},
|
||||
WebAuthN: &session.WebAuthNFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
UserVerified: true,
|
||||
},
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
{ // totp factor
|
||||
Id: "999",
|
||||
CreationDate: timestamppb.New(now),
|
||||
ChangeDate: timestamppb.New(now),
|
||||
Sequence: 123,
|
||||
Factors: &session.Factors{
|
||||
User: &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
OrganizationId: "org1",
|
||||
},
|
||||
Totp: &session.TOTPFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
},
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
}
|
||||
|
||||
out := sessionsToPb(sessions)
|
||||
require.Len(t, out, len(want))
|
||||
|
||||
for i, got := range out {
|
||||
if !proto.Equal(got, want[i]) {
|
||||
t.Errorf("session %d got:\n%v\nwant:\n%v", i, got, want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_userAgentToPb(t *testing.T) {
|
||||
type args struct {
|
||||
ua domain.UserAgent
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *session.UserAgent
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{domain.UserAgent{}},
|
||||
},
|
||||
{
|
||||
name: "fingerprint id and description",
|
||||
args: args{domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fingerPrintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
}},
|
||||
want: &session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fingerPrintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with ip",
|
||||
args: args{domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fingerPrintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
IP: net.IPv4(1, 2, 3, 4),
|
||||
}},
|
||||
want: &session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fingerPrintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
Ip: gu.Ptr("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with header",
|
||||
args: args{domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fingerPrintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
Header: http.Header{
|
||||
"foo": []string{"foo", "bar"},
|
||||
"hello": []string{"world"},
|
||||
},
|
||||
}},
|
||||
want: &session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fingerPrintID"),
|
||||
Description: gu.Ptr("description"),
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"foo": {Values: []string{"foo", "bar"}},
|
||||
"hello": {Values: []string{"world"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := userAgentToPb(tt.args.ua)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewTextQuery(t testing.TB, column query.Column, value string, compare query.TextComparison) query.SearchQuery {
|
||||
q, err := query.NewTextQuery(column, value, compare)
|
||||
require.NoError(t, err)
|
||||
return q
|
||||
}
|
||||
|
||||
func mustNewListQuery(t testing.TB, column query.Column, list []any, compare query.ListComparison) query.SearchQuery {
|
||||
q, err := query.NewListQuery(query.SessionColumnID, list, compare)
|
||||
require.NoError(t, err)
|
||||
return q
|
||||
}
|
||||
|
||||
func mustNewTimestampQuery(t testing.TB, column query.Column, ts time.Time, compare query.TimestampComparison) query.SearchQuery {
|
||||
q, err := query.NewTimestampQuery(column, ts, compare)
|
||||
require.NoError(t, err)
|
||||
return q
|
||||
}
|
||||
|
||||
func Test_listSessionsRequestToQuery(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *session.ListSessionsRequest
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *query.SessionsSearchQueries
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "default request",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("123", "456", "789"),
|
||||
req: &session.ListSessionsRequest{},
|
||||
},
|
||||
want: &query.SessionsSearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 0,
|
||||
Asc: false,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default request with sorting column",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("123", "456", "789"),
|
||||
req: &session.ListSessionsRequest{
|
||||
SortingColumn: session.SessionFieldName_SESSION_FIELD_NAME_CREATION_DATE,
|
||||
},
|
||||
},
|
||||
want: &query.SessionsSearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 0,
|
||||
Limit: 0,
|
||||
SortingColumn: query.SessionColumnCreationDate,
|
||||
Asc: false,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with list query and sessions",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("123", "456", "789"),
|
||||
req: &session.ListSessionsRequest{
|
||||
Query: &object.ListQuery{
|
||||
Offset: 10,
|
||||
Limit: 20,
|
||||
Asc: true,
|
||||
},
|
||||
Queries: []*session.SearchQuery{
|
||||
{Query: &session.SearchQuery_IdsQuery{
|
||||
IdsQuery: &session.IDsQuery{
|
||||
Ids: []string{"1", "2", "3"},
|
||||
},
|
||||
}},
|
||||
{Query: &session.SearchQuery_IdsQuery{
|
||||
IdsQuery: &session.IDsQuery{
|
||||
Ids: []string{"4", "5", "6"},
|
||||
},
|
||||
}},
|
||||
{Query: &session.SearchQuery_UserIdQuery{
|
||||
UserIdQuery: &session.UserIDQuery{
|
||||
Id: "10",
|
||||
},
|
||||
}},
|
||||
{Query: &session.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session.CreationDateQuery{
|
||||
CreationDate: timestamppb.New(creationDate),
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_GREATER,
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &query.SessionsSearchQueries{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: 10,
|
||||
Limit: 20,
|
||||
Asc: true,
|
||||
},
|
||||
Queries: []query.SearchQuery{
|
||||
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
|
||||
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
|
||||
mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
|
||||
mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampGreater),
|
||||
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid argument error",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("123", "456", "789"),
|
||||
req: &session.ListSessionsRequest{
|
||||
Query: &object.ListQuery{
|
||||
Offset: 10,
|
||||
Limit: 20,
|
||||
Asc: true,
|
||||
},
|
||||
Queries: []*session.SearchQuery{
|
||||
{Query: &session.SearchQuery_IdsQuery{
|
||||
IdsQuery: &session.IDsQuery{
|
||||
Ids: []string{"1", "2", "3"},
|
||||
},
|
||||
}},
|
||||
{Query: nil},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := listSessionsRequestToQuery(tt.args.ctx, tt.args.req)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sessionQueriesToQuery(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
queries []*session.SearchQuery
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []query.SearchQuery
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "creator only",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("123", "456", "789"),
|
||||
},
|
||||
want: []query.SearchQuery{
|
||||
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid argument",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("123", "456", "789"),
|
||||
queries: []*session.SearchQuery{
|
||||
{Query: nil},
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "creator and sessions",
|
||||
args: args{
|
||||
ctx: authz.NewMockContext("123", "456", "789"),
|
||||
queries: []*session.SearchQuery{
|
||||
{Query: &session.SearchQuery_IdsQuery{
|
||||
IdsQuery: &session.IDsQuery{
|
||||
Ids: []string{"1", "2", "3"},
|
||||
},
|
||||
}},
|
||||
{Query: &session.SearchQuery_IdsQuery{
|
||||
IdsQuery: &session.IDsQuery{
|
||||
Ids: []string{"4", "5", "6"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
want: []query.SearchQuery{
|
||||
mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
|
||||
mustNewListQuery(t, query.SessionColumnID, []interface{}{"4", "5", "6"}, query.ListIn),
|
||||
mustNewTextQuery(t, query.SessionColumnCreator, "789", query.TextEquals),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := sessionQueriesToQuery(tt.args.ctx, tt.args.queries)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sessionQueryToQuery(t *testing.T) {
|
||||
type args struct {
|
||||
query *session.SearchQuery
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want query.SearchQuery
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid argument",
|
||||
args: args{&session.SearchQuery{
|
||||
Query: nil,
|
||||
}},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "ids query",
|
||||
args: args{&session.SearchQuery{
|
||||
Query: &session.SearchQuery_IdsQuery{
|
||||
IdsQuery: &session.IDsQuery{
|
||||
Ids: []string{"1", "2", "3"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: mustNewListQuery(t, query.SessionColumnID, []interface{}{"1", "2", "3"}, query.ListIn),
|
||||
},
|
||||
{
|
||||
name: "user id query",
|
||||
args: args{&session.SearchQuery{
|
||||
Query: &session.SearchQuery_UserIdQuery{
|
||||
UserIdQuery: &session.UserIDQuery{
|
||||
Id: "10",
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: mustNewTextQuery(t, query.SessionColumnUserID, "10", query.TextEquals),
|
||||
},
|
||||
{
|
||||
name: "creation date query",
|
||||
args: args{&session.SearchQuery{
|
||||
Query: &session.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session.CreationDateQuery{
|
||||
CreationDate: timestamppb.New(creationDate),
|
||||
Method: objpb.TimestampQueryMethod_TIMESTAMP_QUERY_METHOD_LESS,
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampLess),
|
||||
},
|
||||
{
|
||||
name: "creation date query with default method",
|
||||
args: args{&session.SearchQuery{
|
||||
Query: &session.SearchQuery_CreationDateQuery{
|
||||
CreationDateQuery: &session.CreationDateQuery{
|
||||
CreationDate: timestamppb.New(creationDate),
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: mustNewTimestampQuery(t, query.SessionColumnCreationDate, creationDate, query.TimestampEquals),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := sessionQueryToQuery(tt.args.query)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_userCheck(t *testing.T) {
|
||||
type args struct {
|
||||
user *session.CheckUser
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want userSearch
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil user",
|
||||
args: args{nil},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "by user id",
|
||||
args: args{&session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: "foo",
|
||||
},
|
||||
}},
|
||||
want: userSearchByID{"foo"},
|
||||
},
|
||||
{
|
||||
name: "by user id",
|
||||
args: args{&session.CheckUser{
|
||||
Search: &session.CheckUser_LoginName{
|
||||
LoginName: "bar",
|
||||
},
|
||||
}},
|
||||
want: userSearchByLoginName{"bar"},
|
||||
},
|
||||
{
|
||||
name: "unimplemented error",
|
||||
args: args{&session.CheckUser{
|
||||
Search: nil,
|
||||
}},
|
||||
wantErr: zerrors.ThrowUnimplementedf(nil, "SESSION-d3b4g0", "user search %T not implemented", nil),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := userCheck(tt.args.user)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_userVerificationRequirementToDomain(t *testing.T) {
|
||||
type args struct {
|
||||
req session.UserVerificationRequirement
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
want domain.UserVerificationRequirement
|
||||
}{
|
||||
{
|
||||
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED},
|
||||
want: domain.UserVerificationRequirementUnspecified,
|
||||
},
|
||||
{
|
||||
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED},
|
||||
want: domain.UserVerificationRequirementRequired,
|
||||
},
|
||||
{
|
||||
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED},
|
||||
want: domain.UserVerificationRequirementPreferred,
|
||||
},
|
||||
{
|
||||
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED},
|
||||
want: domain.UserVerificationRequirementDiscouraged,
|
||||
},
|
||||
{
|
||||
args: args{999},
|
||||
want: domain.UserVerificationRequirementUnspecified,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.args.req.String(), func(t *testing.T) {
|
||||
got := userVerificationRequirementToDomain(tt.args.req)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_userAgentToCommand(t *testing.T) {
|
||||
type args struct {
|
||||
userAgent *session.UserAgent
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *domain.UserAgent
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{nil},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "all fields",
|
||||
args: args{&session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fp1"),
|
||||
Ip: gu.Ptr("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"hello": {
|
||||
Values: []string{"foo", "bar"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: net.ParseIP("1.2.3.4"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{
|
||||
"hello": []string{"foo", "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid ip",
|
||||
args: args{&session.UserAgent{
|
||||
FingerprintId: gu.Ptr("fp1"),
|
||||
Ip: gu.Ptr("oops"),
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: map[string]*session.UserAgent_HeaderValues{
|
||||
"hello": {
|
||||
Values: []string{"foo", "bar"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: &domain.UserAgent{
|
||||
FingerprintID: gu.Ptr("fp1"),
|
||||
IP: nil,
|
||||
Description: gu.Ptr("firefox"),
|
||||
Header: http.Header{
|
||||
"hello": []string{"foo", "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil fields",
|
||||
args: args{&session.UserAgent{}},
|
||||
want: &domain.UserAgent{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := userAgentToCommand(tt.args.userAgent)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||
)
|
||||
|
||||
var _ settings.SettingsServiceServer = (*Server)(nil)
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||
)
|
||||
|
||||
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||
)
|
||||
|
||||
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||
)
|
||||
|
||||
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"}
|
||||
|
@ -11,8 +11,8 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
|
||||
)
|
||||
|
||||
func TestServer_GetSecuritySettings(t *testing.T) {
|
||||
|
57
internal/api/grpc/settings/v2beta/server.go
Normal file
57
internal/api/grpc/settings/v2beta/server.go
Normal file
@ -0,0 +1,57 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/assets"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
)
|
||||
|
||||
var _ settings.SettingsServiceServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
settings.UnimplementedSettingsServiceServer
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
assetsAPIDomain func(context.Context) string
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
externalSecure bool,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
assetsAPIDomain: assets.AssetAPI(externalSecure),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
|
||||
settings.RegisterSettingsServiceServer(grpcServer, s)
|
||||
}
|
||||
|
||||
func (s *Server) AppName() string {
|
||||
return settings.SettingsService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) MethodPrefix() string {
|
||||
return settings.SettingsService_ServiceDesc.ServiceName
|
||||
}
|
||||
|
||||
func (s *Server) AuthMethods() authz.MethodMapping {
|
||||
return settings.SettingsService_AuthMethods
|
||||
}
|
||||
|
||||
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
|
||||
return settings.RegisterSettingsServiceHandler
|
||||
}
|
34
internal/api/grpc/settings/v2beta/server_integration_test.go
Normal file
34
internal/api/grpc/settings/v2beta/server_integration_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
//go:build integration
|
||||
|
||||
package settings_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX, AdminCTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client settings.SettingsServiceClient
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, _, cancel := integration.Contexts(3 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
defer Tester.Done()
|
||||
|
||||
CTX = ctx
|
||||
AdminCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
|
||||
Client = Tester.Client.SettingsV2beta
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
161
internal/api/grpc/settings/v2beta/settings.go
Normal file
161
internal/api/grpc/settings/v2beta/settings.go
Normal file
@ -0,0 +1,161 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) {
|
||||
current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetLoginSettingsResponse{
|
||||
Settings: loginSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.OrgID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) {
|
||||
current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetPasswordComplexitySettingsResponse{
|
||||
Settings: passwordComplexitySettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) {
|
||||
current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetPasswordExpirySettingsResponse{
|
||||
Settings: passwordExpirySettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) {
|
||||
current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetBrandingSettingsResponse{
|
||||
Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) {
|
||||
current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetDomainSettingsResponse{
|
||||
Settings: domainSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) {
|
||||
current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetLegalAndSupportSettingsResponse{
|
||||
Settings: legalAndSupportSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) {
|
||||
current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetLockoutSettingsResponse{
|
||||
Settings: lockoutSettingsToPb(current),
|
||||
Details: &object_pb.Details{
|
||||
Sequence: current.Sequence,
|
||||
ChangeDate: timestamppb.New(current.ChangeDate),
|
||||
ResourceOwner: current.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) {
|
||||
links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings.GetActiveIdentityProvidersResponse{
|
||||
Details: object.ToListDetails(links.SearchResponse),
|
||||
IdentityProviders: identityProvidersToPb(links.Links),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) {
|
||||
instance := authz.GetInstance(ctx)
|
||||
return &settings.GetGeneralSettingsResponse{
|
||||
SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()),
|
||||
DefaultOrgId: instance.DefaultOrganisationID(),
|
||||
DefaultLanguage: instance.DefaultLanguage().String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) {
|
||||
policy, err := s.query.SecurityPolicy(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.GetSecuritySettingsResponse{
|
||||
Settings: securityPolicyToSettingsPb(policy),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecuritySettingsRequest) (*settings.SetSecuritySettingsResponse, error) {
|
||||
details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings.SetSecuritySettingsResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
245
internal/api/grpc/settings/v2beta/settings_converter.go
Normal file
245
internal/api/grpc/settings/v2beta/settings_converter.go
Normal file
@ -0,0 +1,245 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
)
|
||||
|
||||
func loginSettingsToPb(current *query.LoginPolicy) *settings.LoginSettings {
|
||||
multi := make([]settings.MultiFactorType, len(current.MultiFactors))
|
||||
for i, typ := range current.MultiFactors {
|
||||
multi[i] = multiFactorTypeToPb(typ)
|
||||
}
|
||||
second := make([]settings.SecondFactorType, len(current.SecondFactors))
|
||||
for i, typ := range current.SecondFactors {
|
||||
second[i] = secondFactorTypeToPb(typ)
|
||||
}
|
||||
|
||||
return &settings.LoginSettings{
|
||||
AllowUsernamePassword: current.AllowUsernamePassword,
|
||||
AllowRegister: current.AllowRegister,
|
||||
AllowExternalIdp: current.AllowExternalIDPs,
|
||||
ForceMfa: current.ForceMFA,
|
||||
ForceMfaLocalOnly: current.ForceMFALocalOnly,
|
||||
PasskeysType: passkeysTypeToPb(current.PasswordlessType),
|
||||
HidePasswordReset: current.HidePasswordReset,
|
||||
IgnoreUnknownUsernames: current.IgnoreUnknownUsernames,
|
||||
AllowDomainDiscovery: current.AllowDomainDiscovery,
|
||||
DisableLoginWithEmail: current.DisableLoginWithEmail,
|
||||
DisableLoginWithPhone: current.DisableLoginWithPhone,
|
||||
DefaultRedirectUri: current.DefaultRedirectURI,
|
||||
PasswordCheckLifetime: durationpb.New(time.Duration(current.PasswordCheckLifetime)),
|
||||
ExternalLoginCheckLifetime: durationpb.New(time.Duration(current.ExternalLoginCheckLifetime)),
|
||||
MfaInitSkipLifetime: durationpb.New(time.Duration(current.MFAInitSkipLifetime)),
|
||||
SecondFactorCheckLifetime: durationpb.New(time.Duration(current.SecondFactorCheckLifetime)),
|
||||
MultiFactorCheckLifetime: durationpb.New(time.Duration(current.MultiFactorCheckLifetime)),
|
||||
SecondFactors: second,
|
||||
MultiFactors: multi,
|
||||
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
|
||||
}
|
||||
}
|
||||
|
||||
func isDefaultToResourceOwnerTypePb(isDefault bool) settings.ResourceOwnerType {
|
||||
if isDefault {
|
||||
return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE
|
||||
}
|
||||
return settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG
|
||||
}
|
||||
|
||||
func passkeysTypeToPb(passwordlessType domain.PasswordlessType) settings.PasskeysType {
|
||||
switch passwordlessType {
|
||||
case domain.PasswordlessTypeAllowed:
|
||||
return settings.PasskeysType_PASSKEYS_TYPE_ALLOWED
|
||||
case domain.PasswordlessTypeNotAllowed:
|
||||
return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED
|
||||
default:
|
||||
return settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED
|
||||
}
|
||||
}
|
||||
|
||||
func secondFactorTypeToPb(secondFactorType domain.SecondFactorType) settings.SecondFactorType {
|
||||
switch secondFactorType {
|
||||
case domain.SecondFactorTypeTOTP:
|
||||
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP
|
||||
case domain.SecondFactorTypeU2F:
|
||||
return settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F
|
||||
case domain.SecondFactorTypeOTPEmail:
|
||||
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL
|
||||
case domain.SecondFactorTypeOTPSMS:
|
||||
return settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS
|
||||
case domain.SecondFactorTypeUnspecified:
|
||||
return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
|
||||
default:
|
||||
return settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func multiFactorTypeToPb(typ domain.MultiFactorType) settings.MultiFactorType {
|
||||
switch typ {
|
||||
case domain.MultiFactorTypeU2FWithPIN:
|
||||
return settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION
|
||||
case domain.MultiFactorTypeUnspecified:
|
||||
return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED
|
||||
default:
|
||||
return settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func passwordComplexitySettingsToPb(current *query.PasswordComplexityPolicy) *settings.PasswordComplexitySettings {
|
||||
return &settings.PasswordComplexitySettings{
|
||||
MinLength: current.MinLength,
|
||||
RequiresUppercase: current.HasUppercase,
|
||||
RequiresLowercase: current.HasLowercase,
|
||||
RequiresNumber: current.HasNumber,
|
||||
RequiresSymbol: current.HasSymbol,
|
||||
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
|
||||
}
|
||||
}
|
||||
|
||||
func passwordExpirySettingsToPb(current *query.PasswordAgePolicy) *settings.PasswordExpirySettings {
|
||||
return &settings.PasswordExpirySettings{
|
||||
MaxAgeDays: current.MaxAgeDays,
|
||||
ExpireWarnDays: current.ExpireWarnDays,
|
||||
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
|
||||
}
|
||||
}
|
||||
|
||||
func brandingSettingsToPb(current *query.LabelPolicy, assetPrefix string) *settings.BrandingSettings {
|
||||
return &settings.BrandingSettings{
|
||||
LightTheme: themeToPb(current.Light, assetPrefix, current.ResourceOwner),
|
||||
DarkTheme: themeToPb(current.Dark, assetPrefix, current.ResourceOwner),
|
||||
FontUrl: domain.AssetURL(assetPrefix, current.ResourceOwner, current.FontURL),
|
||||
DisableWatermark: current.WatermarkDisabled,
|
||||
HideLoginNameSuffix: current.HideLoginNameSuffix,
|
||||
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
|
||||
ThemeMode: themeModeToPb(current.ThemeMode),
|
||||
}
|
||||
}
|
||||
|
||||
func themeModeToPb(themeMode domain.LabelPolicyThemeMode) settings.ThemeMode {
|
||||
switch themeMode {
|
||||
case domain.LabelPolicyThemeAuto:
|
||||
return settings.ThemeMode_THEME_MODE_AUTO
|
||||
case domain.LabelPolicyThemeLight:
|
||||
return settings.ThemeMode_THEME_MODE_LIGHT
|
||||
case domain.LabelPolicyThemeDark:
|
||||
return settings.ThemeMode_THEME_MODE_DARK
|
||||
default:
|
||||
return settings.ThemeMode_THEME_MODE_AUTO
|
||||
}
|
||||
}
|
||||
|
||||
func themeToPb(theme query.Theme, assetPrefix, resourceOwner string) *settings.Theme {
|
||||
return &settings.Theme{
|
||||
PrimaryColor: theme.PrimaryColor,
|
||||
BackgroundColor: theme.BackgroundColor,
|
||||
FontColor: theme.FontColor,
|
||||
WarnColor: theme.WarnColor,
|
||||
LogoUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.LogoURL),
|
||||
IconUrl: domain.AssetURL(assetPrefix, resourceOwner, theme.IconURL),
|
||||
}
|
||||
}
|
||||
|
||||
func domainSettingsToPb(current *query.DomainPolicy) *settings.DomainSettings {
|
||||
return &settings.DomainSettings{
|
||||
LoginNameIncludesDomain: current.UserLoginMustBeDomain,
|
||||
RequireOrgDomainVerification: current.ValidateOrgDomains,
|
||||
SmtpSenderAddressMatchesInstanceDomain: current.SMTPSenderAddressMatchesInstanceDomain,
|
||||
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
|
||||
}
|
||||
}
|
||||
|
||||
func legalAndSupportSettingsToPb(current *query.PrivacyPolicy) *settings.LegalAndSupportSettings {
|
||||
return &settings.LegalAndSupportSettings{
|
||||
TosLink: current.TOSLink,
|
||||
PrivacyPolicyLink: current.PrivacyLink,
|
||||
HelpLink: current.HelpLink,
|
||||
SupportEmail: string(current.SupportEmail),
|
||||
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
|
||||
DocsLink: current.DocsLink,
|
||||
CustomLink: current.CustomLink,
|
||||
CustomLinkText: current.CustomLinkText,
|
||||
}
|
||||
}
|
||||
|
||||
func lockoutSettingsToPb(current *query.LockoutPolicy) *settings.LockoutSettings {
|
||||
return &settings.LockoutSettings{
|
||||
MaxPasswordAttempts: current.MaxPasswordAttempts,
|
||||
MaxOtpAttempts: current.MaxOTPAttempts,
|
||||
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
|
||||
}
|
||||
}
|
||||
|
||||
func identityProvidersToPb(idps []*query.IDPLoginPolicyLink) []*settings.IdentityProvider {
|
||||
providers := make([]*settings.IdentityProvider, len(idps))
|
||||
for i, idp := range idps {
|
||||
providers[i] = identityProviderToPb(idp)
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func identityProviderToPb(idp *query.IDPLoginPolicyLink) *settings.IdentityProvider {
|
||||
return &settings.IdentityProvider{
|
||||
Id: idp.IDPID,
|
||||
Name: domain.IDPName(idp.IDPName, idp.IDPType),
|
||||
Type: idpTypeToPb(idp.IDPType),
|
||||
}
|
||||
}
|
||||
|
||||
func idpTypeToPb(idpType domain.IDPType) settings.IdentityProviderType {
|
||||
switch idpType {
|
||||
case domain.IDPTypeUnspecified:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
|
||||
case domain.IDPTypeOIDC:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC
|
||||
case domain.IDPTypeJWT:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT
|
||||
case domain.IDPTypeOAuth:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH
|
||||
case domain.IDPTypeLDAP:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP
|
||||
case domain.IDPTypeAzureAD:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD
|
||||
case domain.IDPTypeGitHub:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB
|
||||
case domain.IDPTypeGitHubEnterprise:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES
|
||||
case domain.IDPTypeGitLab:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB
|
||||
case domain.IDPTypeGitLabSelfHosted:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED
|
||||
case domain.IDPTypeGoogle:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE
|
||||
case domain.IDPTypeSAML:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML
|
||||
case domain.IDPTypeApple:
|
||||
// Handle all remaining cases so the linter succeeds
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
|
||||
default:
|
||||
return settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func securityPolicyToSettingsPb(policy *query.SecurityPolicy) *settings.SecuritySettings {
|
||||
return &settings.SecuritySettings{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: policy.EnableIframeEmbedding,
|
||||
AllowedOrigins: policy.AllowedOrigins,
|
||||
},
|
||||
EnableImpersonation: policy.EnableImpersonation,
|
||||
}
|
||||
}
|
||||
|
||||
func securitySettingsToCommand(req *settings.SetSecuritySettingsRequest) *command.SecurityPolicy {
|
||||
return &command.SecurityPolicy{
|
||||
EnableIframeEmbedding: req.GetEmbeddedIframe().GetEnabled(),
|
||||
AllowedOrigins: req.GetEmbeddedIframe().GetAllowedOrigins(),
|
||||
EnableImpersonation: req.GetEnableImpersonation(),
|
||||
}
|
||||
}
|
517
internal/api/grpc/settings/v2beta/settings_converter_test.go
Normal file
517
internal/api/grpc/settings/v2beta/settings_converter_test.go
Normal file
@ -0,0 +1,517 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
)
|
||||
|
||||
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"}
|
||||
|
||||
func Test_loginSettingsToPb(t *testing.T) {
|
||||
arg := &query.LoginPolicy{
|
||||
AllowUsernamePassword: true,
|
||||
AllowRegister: true,
|
||||
AllowExternalIDPs: true,
|
||||
ForceMFA: true,
|
||||
ForceMFALocalOnly: true,
|
||||
PasswordlessType: domain.PasswordlessTypeAllowed,
|
||||
HidePasswordReset: true,
|
||||
IgnoreUnknownUsernames: true,
|
||||
AllowDomainDiscovery: true,
|
||||
DisableLoginWithEmail: true,
|
||||
DisableLoginWithPhone: true,
|
||||
DefaultRedirectURI: "example.com",
|
||||
PasswordCheckLifetime: database.Duration(time.Hour),
|
||||
ExternalLoginCheckLifetime: database.Duration(time.Minute),
|
||||
MFAInitSkipLifetime: database.Duration(time.Millisecond),
|
||||
SecondFactorCheckLifetime: database.Duration(time.Microsecond),
|
||||
MultiFactorCheckLifetime: database.Duration(time.Nanosecond),
|
||||
SecondFactors: []domain.SecondFactorType{
|
||||
domain.SecondFactorTypeTOTP,
|
||||
domain.SecondFactorTypeU2F,
|
||||
domain.SecondFactorTypeOTPEmail,
|
||||
domain.SecondFactorTypeOTPSMS,
|
||||
},
|
||||
MultiFactors: []domain.MultiFactorType{
|
||||
domain.MultiFactorTypeU2FWithPIN,
|
||||
},
|
||||
IsDefault: true,
|
||||
}
|
||||
|
||||
want := &settings.LoginSettings{
|
||||
AllowUsernamePassword: true,
|
||||
AllowRegister: true,
|
||||
AllowExternalIdp: true,
|
||||
ForceMfa: true,
|
||||
ForceMfaLocalOnly: true,
|
||||
PasskeysType: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
|
||||
HidePasswordReset: true,
|
||||
IgnoreUnknownUsernames: true,
|
||||
AllowDomainDiscovery: true,
|
||||
DisableLoginWithEmail: true,
|
||||
DisableLoginWithPhone: true,
|
||||
DefaultRedirectUri: "example.com",
|
||||
PasswordCheckLifetime: durationpb.New(time.Hour),
|
||||
ExternalLoginCheckLifetime: durationpb.New(time.Minute),
|
||||
MfaInitSkipLifetime: durationpb.New(time.Millisecond),
|
||||
SecondFactorCheckLifetime: durationpb.New(time.Microsecond),
|
||||
MultiFactorCheckLifetime: durationpb.New(time.Nanosecond),
|
||||
SecondFactors: []settings.SecondFactorType{
|
||||
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
|
||||
settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
|
||||
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL,
|
||||
settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS,
|
||||
},
|
||||
MultiFactors: []settings.MultiFactorType{
|
||||
settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION,
|
||||
},
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
|
||||
got := loginSettingsToPb(arg)
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("loginSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isDefaultToResourceOwnerTypePb(t *testing.T) {
|
||||
type args struct {
|
||||
isDefault bool
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
want settings.ResourceOwnerType
|
||||
}{
|
||||
{
|
||||
args: args{false},
|
||||
want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_ORG,
|
||||
},
|
||||
{
|
||||
args: args{true},
|
||||
want: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want.String(), func(t *testing.T) {
|
||||
got := isDefaultToResourceOwnerTypePb(tt.args.isDefault)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_passkeysTypeToPb(t *testing.T) {
|
||||
type args struct {
|
||||
passwordlessType domain.PasswordlessType
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
want settings.PasskeysType
|
||||
}{
|
||||
{
|
||||
args: args{domain.PasswordlessTypeNotAllowed},
|
||||
want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED,
|
||||
},
|
||||
{
|
||||
args: args{domain.PasswordlessTypeAllowed},
|
||||
want: settings.PasskeysType_PASSKEYS_TYPE_ALLOWED,
|
||||
},
|
||||
{
|
||||
args: args{99},
|
||||
want: settings.PasskeysType_PASSKEYS_TYPE_NOT_ALLOWED,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want.String(), func(t *testing.T) {
|
||||
got := passkeysTypeToPb(tt.args.passwordlessType)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_secondFactorTypeToPb(t *testing.T) {
|
||||
type args struct {
|
||||
secondFactorType domain.SecondFactorType
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
want settings.SecondFactorType
|
||||
}{
|
||||
{
|
||||
args: args{domain.SecondFactorTypeTOTP},
|
||||
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP,
|
||||
},
|
||||
{
|
||||
args: args{domain.SecondFactorTypeU2F},
|
||||
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_U2F,
|
||||
},
|
||||
{
|
||||
args: args{domain.SecondFactorTypeOTPSMS},
|
||||
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_SMS,
|
||||
},
|
||||
{
|
||||
args: args{domain.SecondFactorTypeOTPEmail},
|
||||
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_OTP_EMAIL,
|
||||
},
|
||||
{
|
||||
args: args{domain.SecondFactorTypeUnspecified},
|
||||
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
args: args{99},
|
||||
want: settings.SecondFactorType_SECOND_FACTOR_TYPE_UNSPECIFIED,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want.String(), func(t *testing.T) {
|
||||
got := secondFactorTypeToPb(tt.args.secondFactorType)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_multiFactorTypeToPb(t *testing.T) {
|
||||
type args struct {
|
||||
typ domain.MultiFactorType
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
want settings.MultiFactorType
|
||||
}{
|
||||
{
|
||||
args: args{domain.MultiFactorTypeU2FWithPIN},
|
||||
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION,
|
||||
},
|
||||
{
|
||||
args: args{domain.MultiFactorTypeUnspecified},
|
||||
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
args: args{99},
|
||||
want: settings.MultiFactorType_MULTI_FACTOR_TYPE_UNSPECIFIED,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want.String(), func(t *testing.T) {
|
||||
got := multiFactorTypeToPb(tt.args.typ)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_passwordComplexitySettingsToPb(t *testing.T) {
|
||||
arg := &query.PasswordComplexityPolicy{
|
||||
MinLength: 12,
|
||||
HasUppercase: true,
|
||||
HasLowercase: true,
|
||||
HasNumber: true,
|
||||
HasSymbol: true,
|
||||
IsDefault: true,
|
||||
}
|
||||
want := &settings.PasswordComplexitySettings{
|
||||
MinLength: 12,
|
||||
RequiresUppercase: true,
|
||||
RequiresLowercase: true,
|
||||
RequiresNumber: true,
|
||||
RequiresSymbol: true,
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
|
||||
got := passwordComplexitySettingsToPb(arg)
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("passwordComplexitySettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_passwordExpirySettingsToPb(t *testing.T) {
|
||||
arg := &query.PasswordAgePolicy{
|
||||
ExpireWarnDays: 80,
|
||||
MaxAgeDays: 90,
|
||||
IsDefault: true,
|
||||
}
|
||||
want := &settings.PasswordExpirySettings{
|
||||
ExpireWarnDays: 80,
|
||||
MaxAgeDays: 90,
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
|
||||
got := passwordExpirySettingsToPb(arg)
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("passwordExpirySettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_brandingSettingsToPb(t *testing.T) {
|
||||
arg := &query.LabelPolicy{
|
||||
Light: query.Theme{
|
||||
PrimaryColor: "red",
|
||||
WarnColor: "white",
|
||||
BackgroundColor: "blue",
|
||||
FontColor: "orange",
|
||||
LogoURL: "light-logo",
|
||||
IconURL: "light-icon",
|
||||
},
|
||||
Dark: query.Theme{
|
||||
PrimaryColor: "magenta",
|
||||
WarnColor: "pink",
|
||||
BackgroundColor: "black",
|
||||
FontColor: "white",
|
||||
LogoURL: "dark-logo",
|
||||
IconURL: "dark-icon",
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
FontURL: "fonts",
|
||||
WatermarkDisabled: true,
|
||||
HideLoginNameSuffix: true,
|
||||
ThemeMode: domain.LabelPolicyThemeDark,
|
||||
IsDefault: true,
|
||||
}
|
||||
want := &settings.BrandingSettings{
|
||||
LightTheme: &settings.Theme{
|
||||
PrimaryColor: "red",
|
||||
WarnColor: "white",
|
||||
BackgroundColor: "blue",
|
||||
FontColor: "orange",
|
||||
LogoUrl: "http://example.com/me/light-logo",
|
||||
IconUrl: "http://example.com/me/light-icon",
|
||||
},
|
||||
DarkTheme: &settings.Theme{
|
||||
PrimaryColor: "magenta",
|
||||
WarnColor: "pink",
|
||||
BackgroundColor: "black",
|
||||
FontColor: "white",
|
||||
LogoUrl: "http://example.com/me/dark-logo",
|
||||
IconUrl: "http://example.com/me/dark-icon",
|
||||
},
|
||||
FontUrl: "http://example.com/me/fonts",
|
||||
DisableWatermark: true,
|
||||
HideLoginNameSuffix: true,
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
ThemeMode: settings.ThemeMode_THEME_MODE_DARK,
|
||||
}
|
||||
|
||||
got := brandingSettingsToPb(arg, "http://example.com")
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("brandingSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_domainSettingsToPb(t *testing.T) {
|
||||
arg := &query.DomainPolicy{
|
||||
UserLoginMustBeDomain: true,
|
||||
ValidateOrgDomains: true,
|
||||
SMTPSenderAddressMatchesInstanceDomain: true,
|
||||
IsDefault: true,
|
||||
}
|
||||
want := &settings.DomainSettings{
|
||||
LoginNameIncludesDomain: true,
|
||||
RequireOrgDomainVerification: true,
|
||||
SmtpSenderAddressMatchesInstanceDomain: true,
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
got := domainSettingsToPb(arg)
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("domainSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_legalSettingsToPb(t *testing.T) {
|
||||
arg := &query.PrivacyPolicy{
|
||||
TOSLink: "http://example.com/tos",
|
||||
PrivacyLink: "http://example.com/pricacy",
|
||||
HelpLink: "http://example.com/help",
|
||||
SupportEmail: "support@zitadel.com",
|
||||
IsDefault: true,
|
||||
DocsLink: "http://example.com/docs",
|
||||
CustomLink: "http://example.com/custom",
|
||||
CustomLinkText: "Custom",
|
||||
}
|
||||
want := &settings.LegalAndSupportSettings{
|
||||
TosLink: "http://example.com/tos",
|
||||
PrivacyPolicyLink: "http://example.com/pricacy",
|
||||
HelpLink: "http://example.com/help",
|
||||
SupportEmail: "support@zitadel.com",
|
||||
DocsLink: "http://example.com/docs",
|
||||
CustomLink: "http://example.com/custom",
|
||||
CustomLinkText: "Custom",
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
got := legalAndSupportSettingsToPb(arg)
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("legalSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_lockoutSettingsToPb(t *testing.T) {
|
||||
arg := &query.LockoutPolicy{
|
||||
MaxPasswordAttempts: 22,
|
||||
MaxOTPAttempts: 22,
|
||||
IsDefault: true,
|
||||
}
|
||||
want := &settings.LockoutSettings{
|
||||
MaxPasswordAttempts: 22,
|
||||
MaxOtpAttempts: 22,
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
got := lockoutSettingsToPb(arg)
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("lockoutSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_identityProvidersToPb(t *testing.T) {
|
||||
arg := []*query.IDPLoginPolicyLink{
|
||||
{
|
||||
IDPID: "1",
|
||||
IDPName: "foo",
|
||||
IDPType: domain.IDPTypeOIDC,
|
||||
},
|
||||
{
|
||||
IDPID: "2",
|
||||
IDPName: "bar",
|
||||
IDPType: domain.IDPTypeGitHub,
|
||||
},
|
||||
}
|
||||
want := []*settings.IdentityProvider{
|
||||
{
|
||||
Id: "1",
|
||||
Name: "foo",
|
||||
Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC,
|
||||
},
|
||||
{
|
||||
Id: "2",
|
||||
Name: "bar",
|
||||
Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB,
|
||||
},
|
||||
}
|
||||
got := identityProvidersToPb(arg)
|
||||
require.Len(t, got, len(got))
|
||||
for i, v := range got {
|
||||
grpc.AllFieldsSet(t, v.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(v, want[i]) {
|
||||
t.Errorf("identityProvidersToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_idpTypeToPb(t *testing.T) {
|
||||
type args struct {
|
||||
idpType domain.IDPType
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
want settings.IdentityProviderType
|
||||
}{
|
||||
{
|
||||
args: args{domain.IDPTypeUnspecified},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeOIDC},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OIDC,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeJWT},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_JWT,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeOAuth},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeLDAP},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_LDAP,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeAzureAD},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_AZURE_AD,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeGitHub},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeGitHubEnterprise},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITHUB_ES,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeGitLab},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeGitLabSelfHosted},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GITLAB_SELF_HOSTED,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeGoogle},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_GOOGLE,
|
||||
},
|
||||
{
|
||||
args: args{domain.IDPTypeSAML},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_SAML,
|
||||
},
|
||||
{
|
||||
args: args{99},
|
||||
want: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_UNSPECIFIED,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want.String(), func(t *testing.T) {
|
||||
if got := idpTypeToPb(tt.args.idpType); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("idpTypeToPb() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_securityPolicyToSettingsPb(t *testing.T) {
|
||||
want := &settings.SecuritySettings{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"foo", "bar"},
|
||||
},
|
||||
EnableImpersonation: true,
|
||||
}
|
||||
got := securityPolicyToSettingsPb(&query.SecurityPolicy{
|
||||
EnableIframeEmbedding: true,
|
||||
AllowedOrigins: []string{"foo", "bar"},
|
||||
EnableImpersonation: true,
|
||||
})
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func Test_securitySettingsToCommand(t *testing.T) {
|
||||
want := &command.SecurityPolicy{
|
||||
EnableIframeEmbedding: true,
|
||||
AllowedOrigins: []string{"foo", "bar"},
|
||||
EnableImpersonation: true,
|
||||
}
|
||||
got := securitySettingsToCommand(&settings.SetSecuritySettingsRequest{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"foo", "bar"},
|
||||
},
|
||||
EnableImpersonation: true,
|
||||
})
|
||||
assert.Equal(t, want, got)
|
||||
}
|
174
internal/api/grpc/settings/v2beta/settings_integration_test.go
Normal file
174
internal/api/grpc/settings/v2beta/settings_integration_test.go
Normal file
@ -0,0 +1,174 @@
|
||||
//go:build integration
|
||||
|
||||
package settings_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_GetSecuritySettings(t *testing.T) {
|
||||
_, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"foo", "bar"},
|
||||
},
|
||||
EnableImpersonation: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
want *settings.GetSecuritySettingsResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
ctx: AdminCTX,
|
||||
want: &settings.GetSecuritySettingsResponse{
|
||||
Settings: &settings.SecuritySettings{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"foo", "bar"},
|
||||
},
|
||||
EnableImpersonation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{})
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
got, want := resp.GetSettings(), tt.want.GetSettings()
|
||||
assert.Equal(t, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding")
|
||||
assert.Equal(t, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins")
|
||||
assert.Equal(t, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_SetSecuritySettings(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *settings.SetSecuritySettingsRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *settings.SetSecuritySettingsResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "permission error",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorization(CTX, integration.OrgOwner),
|
||||
req: &settings.SetSecuritySettingsRequest{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"foo.com", "bar.com"},
|
||||
},
|
||||
EnableImpersonation: true,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success allowed origins",
|
||||
args: args{
|
||||
ctx: AdminCTX,
|
||||
req: &settings.SetSecuritySettingsRequest{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
AllowedOrigins: []string{"foo.com", "bar.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &settings.SetSecuritySettingsResponse{
|
||||
Details: &object_pb.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success enable iframe embedding",
|
||||
args: args{
|
||||
ctx: AdminCTX,
|
||||
req: &settings.SetSecuritySettingsRequest{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &settings.SetSecuritySettingsResponse{
|
||||
Details: &object_pb.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success impersonation",
|
||||
args: args{
|
||||
ctx: AdminCTX,
|
||||
req: &settings.SetSecuritySettingsRequest{
|
||||
EnableImpersonation: true,
|
||||
},
|
||||
},
|
||||
want: &settings.SetSecuritySettingsResponse{
|
||||
Details: &object_pb.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success all",
|
||||
args: args{
|
||||
ctx: AdminCTX,
|
||||
req: &settings.SetSecuritySettingsRequest{
|
||||
EmbeddedIframe: &settings.EmbeddedIframeSettings{
|
||||
Enabled: true,
|
||||
AllowedOrigins: []string{"foo.com", "bar.com"},
|
||||
},
|
||||
EnableImpersonation: true,
|
||||
},
|
||||
},
|
||||
want: &settings.SetSecuritySettingsResponse{
|
||||
Details: &object_pb.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Instance.InstanceID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.SetSecuritySettings(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
|
||||
)
|
||||
@ -255,22 +254,6 @@ func UserAuthMethodToWebAuthNTokenPb(token *query.AuthMethod) *user_pb.WebAuthNT
|
||||
}
|
||||
}
|
||||
|
||||
func ExternalIDPViewsToExternalIDPs(externalIDPs []*query.IDPUserLink) []*domain.UserIDPLink {
|
||||
idps := make([]*domain.UserIDPLink, len(externalIDPs))
|
||||
for i, idp := range externalIDPs {
|
||||
idps[i] = &domain.UserIDPLink{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: idp.UserID,
|
||||
ResourceOwner: idp.ResourceOwner,
|
||||
},
|
||||
IDPConfigID: idp.IDPID,
|
||||
ExternalUserID: idp.ProvidedUserID,
|
||||
DisplayName: idp.ProvidedUsername,
|
||||
}
|
||||
}
|
||||
return idps
|
||||
}
|
||||
|
||||
func TypeToPb(userType domain.UserType) user_pb.Type {
|
||||
switch userType {
|
||||
case domain.UserTypeHuman:
|
||||
|
@ -17,8 +17,8 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
schema "github.com/zitadel/zitadel/pkg/grpc/user/schema/v3alpha"
|
||||
)
|
||||
|
||||
|
@ -7,8 +7,8 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||
|
@ -12,9 +12,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_SetEmail(t *testing.T) {
|
||||
|
94
internal/api/grpc/user/v2/idp_link.go
Normal file
94
internal/api/grpc/user/v2/idp_link.go
Normal file
@ -0,0 +1,94 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
|
||||
details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{
|
||||
IDPID: req.GetIdpLink().GetIdpId(),
|
||||
DisplayName: req.GetIdpLink().GetUserName(),
|
||||
IDPExternalID: req.GetIdpLink().GetUserId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddIDPLinkResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest) (_ *user.ListIDPLinksResponse, err error) {
|
||||
queries, err := ListLinkedIDPsRequestToQuery(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := s.query.IDPUserLinks(ctx, queries, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.RemoveNoPermission(ctx, s.checkPermission)
|
||||
return &user.ListIDPLinksResponse{
|
||||
Result: IDPLinksToPb(res.Links),
|
||||
Details: object.ToListDetails(res.SearchResponse),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ListLinkedIDPsRequestToQuery(req *user.ListIDPLinksRequest) (*query.IDPUserLinksSearchQuery, error) {
|
||||
offset, limit, asc := object.ListQueryToQuery(req.Query)
|
||||
userQuery, err := query.NewIDPUserLinksUserIDSearchQuery(req.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &query.IDPUserLinksSearchQuery{
|
||||
SearchRequest: query.SearchRequest{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Asc: asc,
|
||||
},
|
||||
Queries: []query.SearchQuery{userQuery},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func IDPLinksToPb(res []*query.IDPUserLink) []*user.IDPLink {
|
||||
links := make([]*user.IDPLink, len(res))
|
||||
for i, link := range res {
|
||||
links[i] = IDPLinkToPb(link)
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
func IDPLinkToPb(link *query.IDPUserLink) *user.IDPLink {
|
||||
return &user.IDPLink{
|
||||
IdpId: link.IDPID,
|
||||
UserId: link.ProvidedUserID,
|
||||
UserName: link.ProvidedUsername,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RemoveIDPLink(ctx context.Context, req *user.RemoveIDPLinkRequest) (*user.RemoveIDPLinkResponse, error) {
|
||||
objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveIDPLinkRequestToDomain(ctx, req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RemoveIDPLinkResponse{
|
||||
Details: object.DomainToDetailsPb(objectDetails),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RemoveIDPLinkRequestToDomain(ctx context.Context, req *user.RemoveIDPLinkRequest) *domain.UserIDPLink {
|
||||
return &domain.UserIDPLink{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: req.UserId,
|
||||
},
|
||||
IDPConfigID: req.IdpId,
|
||||
ExternalUserID: req.LinkedUserId,
|
||||
}
|
||||
}
|
360
internal/api/grpc/user/v2/idp_link_integration_test.go
Normal file
360
internal/api/grpc/user/v2/idp_link_integration_test.go
Normal file
@ -0,0 +1,360 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func TestServer_AddIDPLink(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddIDPLinkRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.AddIDPLinkResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "user does not exist",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: "userID",
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
UserId: "userID",
|
||||
UserName: "username",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "idp does not exist",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: "idpID",
|
||||
UserId: "userID",
|
||||
UserName: "username",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add link",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
UserId: "userID",
|
||||
UserName: "username",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.AddIDPLinkResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ListIDPLinks(t *testing.T) {
|
||||
orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
|
||||
|
||||
instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX)
|
||||
userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
|
||||
Tester.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpID, "externalUsername_instance")
|
||||
|
||||
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, IamCTX, orgResp.OrganizationId)
|
||||
userOrgResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
|
||||
Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org")
|
||||
|
||||
userMultipleResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
|
||||
Tester.CreateUserIDPlink(IamCTX, userMultipleResp.GetUserId(), "external_multi", instanceIdpID, "externalUsername_multi")
|
||||
Tester.CreateUserIDPlink(IamCTX, userMultipleResp.GetUserId(), "external_multi", orgIdpID, "externalUsername_multi")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.ListIDPLinksRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.ListIDPLinksResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "list links, no permission",
|
||||
args: args{
|
||||
UserCTX,
|
||||
&user.ListIDPLinksRequest{
|
||||
UserId: userOrgResp.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.ListIDPLinksResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.IDPLink{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list links, no permission, org",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.ListIDPLinksRequest{
|
||||
UserId: userOrgResp.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.ListIDPLinksResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.IDPLink{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list idp links, org, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListIDPLinksRequest{
|
||||
UserId: userOrgResp.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.ListIDPLinksResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.IDPLink{
|
||||
{
|
||||
IdpId: orgIdpID,
|
||||
UserId: "external_org",
|
||||
UserName: "externalUsername_org",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list idp links, instance, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListIDPLinksRequest{
|
||||
UserId: userInstanceResp.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.ListIDPLinksResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.IDPLink{
|
||||
{
|
||||
IdpId: instanceIdpID,
|
||||
UserId: "external_instance",
|
||||
UserName: "externalUsername_instance",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list idp links, multi, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListIDPLinksRequest{
|
||||
UserId: userMultipleResp.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.ListIDPLinksResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 2,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.IDPLink{
|
||||
{
|
||||
IdpId: instanceIdpID,
|
||||
UserId: "external_multi",
|
||||
UserName: "externalUsername_multi",
|
||||
},
|
||||
{
|
||||
IdpId: orgIdpID,
|
||||
UserId: "external_multi",
|
||||
UserName: "externalUsername_multi",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
retryDuration := time.Minute
|
||||
if ctxDeadline, ok := CTX.Deadline(); ok {
|
||||
retryDuration = time.Until(ctxDeadline)
|
||||
}
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, listErr := Client.ListIDPLinks(tt.args.ctx, tt.args.req)
|
||||
assertErr := assert.NoError
|
||||
if tt.wantErr {
|
||||
assertErr = assert.Error
|
||||
}
|
||||
assertErr(ttt, listErr)
|
||||
if listErr != nil {
|
||||
return
|
||||
}
|
||||
// always first check length, otherwise its failed anyway
|
||||
assert.Len(ttt, got.Result, len(tt.want.Result))
|
||||
for i := range tt.want.Result {
|
||||
assert.Contains(ttt, got.Result, tt.want.Result[i])
|
||||
}
|
||||
integration.AssertListDetails(t, tt.want, got)
|
||||
}, retryDuration, time.Millisecond*100, "timeout waiting for expected idplinks result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RemoveIDPLink(t *testing.T) {
|
||||
orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano()))
|
||||
|
||||
instanceIdpID := Tester.AddGenericOAuthProvider(t, IamCTX)
|
||||
userInstanceResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
|
||||
Tester.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpID, "externalUsername_instance")
|
||||
|
||||
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, IamCTX, orgResp.OrganizationId)
|
||||
userOrgResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
|
||||
Tester.CreateUserIDPlink(IamCTX, userOrgResp.GetUserId(), "external_org", orgIdpID, "externalUsername_org")
|
||||
|
||||
userNoLinkResp := Tester.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, fmt.Sprintf("%d@listidplinks.com", time.Now().UnixNano()))
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RemoveIDPLinkRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RemoveIDPLinkResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "remove link, no permission",
|
||||
args: args{
|
||||
UserCTX,
|
||||
&user.RemoveIDPLinkRequest{
|
||||
UserId: userOrgResp.GetUserId(),
|
||||
IdpId: orgIdpID,
|
||||
LinkedUserId: "external_org",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "remove link, no permission, org",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.RemoveIDPLinkRequest{
|
||||
UserId: userOrgResp.GetUserId(),
|
||||
IdpId: orgIdpID,
|
||||
LinkedUserId: "external_org",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "remove link, org, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.RemoveIDPLinkRequest{
|
||||
UserId: userOrgResp.GetUserId(),
|
||||
IdpId: orgIdpID,
|
||||
LinkedUserId: "external_org",
|
||||
},
|
||||
},
|
||||
want: &user.RemoveIDPLinkResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: orgResp.GetOrganizationId(),
|
||||
ChangeDate: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove link, instance, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.RemoveIDPLinkRequest{
|
||||
UserId: userInstanceResp.GetUserId(),
|
||||
IdpId: instanceIdpID,
|
||||
LinkedUserId: "external_instance",
|
||||
},
|
||||
},
|
||||
want: &user.RemoveIDPLinkResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: orgResp.GetOrganizationId(),
|
||||
ChangeDate: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove link, no link, error",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.RemoveIDPLinkRequest{
|
||||
UserId: userNoLinkResp.GetUserId(),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RemoveIDPLink(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
|
||||
|
@ -9,9 +9,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_AddOTPSMS(t *testing.T) {
|
||||
|
@ -7,9 +7,10 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
|
||||
@ -116,3 +117,68 @@ func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*use
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RemovePasskey(ctx context.Context, req *user.RemovePasskeyRequest) (*user.RemovePasskeyResponse, error) {
|
||||
objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.GetUserId(), req.GetPasskeyId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RemovePasskeyResponse{
|
||||
Details: object.DomainToDetailsPb(objectDetails),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest) (*user.ListPasskeysResponse, error) {
|
||||
query := new(query.UserAuthMethodSearchQueries)
|
||||
err := query.AppendUserIDQuery(req.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = query.AppendAuthMethodQuery(domain.UserAuthMethodTypePasswordless)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = query.AppendStateQuery(domain.MFAStateReady)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authMethods, err := s.query.SearchUserAuthMethods(ctx, query, false)
|
||||
authMethods.RemoveNoPermission(ctx, s.checkPermission)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.ListPasskeysResponse{
|
||||
Details: object.ToListDetails(authMethods.SearchResponse),
|
||||
Result: authMethodsToPasskeyPb(authMethods),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authMethodsToPasskeyPb(methods *query.AuthMethods) []*user.Passkey {
|
||||
t := make([]*user.Passkey, len(methods.AuthMethods))
|
||||
for i, token := range methods.AuthMethods {
|
||||
t[i] = authMethodToPasskeyPb(token)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func authMethodToPasskeyPb(token *query.AuthMethod) *user.Passkey {
|
||||
return &user.Passkey{
|
||||
Id: token.TokenID,
|
||||
State: mfaStateToPb(token.State),
|
||||
Name: token.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func mfaStateToPb(state domain.MFAState) user.AuthFactorState {
|
||||
switch state {
|
||||
case domain.MFAStateNotReady:
|
||||
return user.AuthFactorState_AUTH_FACTOR_STATE_NOT_READY
|
||||
case domain.MFAStateReady:
|
||||
return user.AuthFactorState_AUTH_FACTOR_STATE_READY
|
||||
case domain.MFAStateUnspecified, domain.MFAStateRemoved:
|
||||
// Handle all remaining cases so the linter succeeds
|
||||
return user.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED
|
||||
default:
|
||||
return user.AuthFactorState_AUTH_FACTOR_STATE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ package user_test
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -12,9 +13,10 @@ import (
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RegisterPasskey(t *testing.T) {
|
||||
@ -138,19 +140,7 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkr.GetPasskeyId())
|
||||
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
|
||||
userID, pkr := userWithPasskeyRegistered(t)
|
||||
|
||||
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
@ -317,3 +307,291 @@ func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func userWithPasskeyRegistered(t *testing.T) (string, *user.RegisterPasskeyResponse) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
return userID, passkeyRegister(t, userID)
|
||||
}
|
||||
|
||||
func userWithPasskeyVerified(t *testing.T) (string, string) {
|
||||
userID, pkr := userWithPasskeyRegistered(t)
|
||||
return userID, passkeyVerify(t, userID, pkr)
|
||||
}
|
||||
|
||||
func passkeyRegister(t *testing.T, userID string) *user.RegisterPasskeyResponse {
|
||||
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkr.GetPasskeyId())
|
||||
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
|
||||
return pkr
|
||||
}
|
||||
|
||||
func passkeyVerify(t *testing.T, userID string, pkr *user.RegisterPasskeyResponse) string {
|
||||
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Client.VerifyPasskeyRegistration(CTX, &user.VerifyPasskeyRegistrationRequest{
|
||||
UserId: userID,
|
||||
PasskeyId: pkr.GetPasskeyId(),
|
||||
PublicKeyCredential: attestationResponse,
|
||||
PasskeyName: "nice name",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return pkr.GetPasskeyId()
|
||||
}
|
||||
|
||||
func TestServer_RemovePasskey(t *testing.T) {
|
||||
userIDWithout := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
userIDRegistered, pkrRegistered := userWithPasskeyRegistered(t)
|
||||
userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t)
|
||||
userIDVerifiedPermission, passkeyIDVerifiedPermission := userWithPasskeyVerified(t)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RemovePasskeyRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RemovePasskeyResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &user.RemovePasskeyRequest{
|
||||
PasskeyId: "123",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing passkey id",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &user.RemovePasskeyRequest{
|
||||
UserId: "123",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success, registered",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &user.RemovePasskeyRequest{
|
||||
UserId: userIDRegistered,
|
||||
PasskeyId: pkrRegistered.GetPasskeyId(),
|
||||
},
|
||||
},
|
||||
want: &user.RemovePasskeyResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no passkey, error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &user.RemovePasskeyRequest{
|
||||
UserId: userIDWithout,
|
||||
PasskeyId: pkrRegistered.GetPasskeyId(),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success, verified",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &user.RemovePasskeyRequest{
|
||||
UserId: userIDVerified,
|
||||
PasskeyId: passkeyIDVerified,
|
||||
},
|
||||
},
|
||||
want: &user.RemovePasskeyResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verified, permission error",
|
||||
args: args{
|
||||
ctx: UserCTX,
|
||||
req: &user.RemovePasskeyRequest{
|
||||
UserId: userIDVerifiedPermission,
|
||||
PasskeyId: passkeyIDVerifiedPermission,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RemovePasskey(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ListPasskeys(t *testing.T) {
|
||||
userIDWithout := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
userIDRegistered, _ := userWithPasskeyRegistered(t)
|
||||
userIDVerified, passkeyIDVerified := userWithPasskeyVerified(t)
|
||||
|
||||
userIDMulti, passkeyIDMulti1 := userWithPasskeyVerified(t)
|
||||
passkeyIDMulti2 := passkeyVerify(t, userIDMulti, passkeyRegister(t, userIDMulti))
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.ListPasskeysRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.ListPasskeysResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "list passkeys, no permission",
|
||||
args: args{
|
||||
UserCTX,
|
||||
&user.ListPasskeysRequest{
|
||||
UserId: userIDVerified,
|
||||
},
|
||||
},
|
||||
want: &user.ListPasskeysResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.Passkey{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list passkeys, none",
|
||||
args: args{
|
||||
UserCTX,
|
||||
&user.ListPasskeysRequest{
|
||||
UserId: userIDWithout,
|
||||
},
|
||||
},
|
||||
want: &user.ListPasskeysResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.Passkey{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list passkeys, registered",
|
||||
args: args{
|
||||
UserCTX,
|
||||
&user.ListPasskeysRequest{
|
||||
UserId: userIDRegistered,
|
||||
},
|
||||
},
|
||||
want: &user.ListPasskeysResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 0,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.Passkey{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list passkeys, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListPasskeysRequest{
|
||||
UserId: userIDVerified,
|
||||
},
|
||||
},
|
||||
want: &user.ListPasskeysResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 1,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.Passkey{
|
||||
{
|
||||
Id: passkeyIDVerified,
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
Name: "nice name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list idp links, multi, ok",
|
||||
args: args{
|
||||
IamCTX,
|
||||
&user.ListPasskeysRequest{
|
||||
UserId: userIDMulti,
|
||||
},
|
||||
},
|
||||
want: &user.ListPasskeysResponse{
|
||||
Details: &object.ListDetails{
|
||||
TotalResult: 2,
|
||||
Timestamp: timestamppb.Now(),
|
||||
},
|
||||
Result: []*user.Passkey{
|
||||
{
|
||||
Id: passkeyIDMulti1,
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
Name: "nice name",
|
||||
},
|
||||
{
|
||||
Id: passkeyIDMulti2,
|
||||
State: user.AuthFactorState_AUTH_FACTOR_STATE_READY,
|
||||
Name: "nice name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
retryDuration := time.Minute
|
||||
if ctxDeadline, ok := CTX.Deadline(); ok {
|
||||
retryDuration = time.Until(ctxDeadline)
|
||||
}
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
got, listErr := Client.ListPasskeys(tt.args.ctx, tt.args.req)
|
||||
assertErr := assert.NoError
|
||||
if tt.wantErr {
|
||||
assertErr = assert.Error
|
||||
}
|
||||
assertErr(ttt, listErr)
|
||||
if listErr != nil {
|
||||
return
|
||||
}
|
||||
// always first check length, otherwise its failed anyway
|
||||
assert.Len(ttt, got.Result, len(tt.want.Result))
|
||||
for i := range tt.want.Result {
|
||||
assert.Contains(ttt, got.Result, tt.want.Result[i])
|
||||
}
|
||||
integration.AssertListDetails(t, tt.want, got)
|
||||
}, retryDuration, time.Millisecond*100, "timeout waiting for expected idplinks result")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) {
|
||||
|
@ -11,9 +11,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RequestPasswordReset(t *testing.T) {
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func Test_notificationTypeToDomain(t *testing.T) {
|
||||
|
@ -7,8 +7,8 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {
|
||||
|
@ -13,9 +13,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_SetPhone(t *testing.T) {
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) {
|
||||
|
@ -13,9 +13,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_GetUserByID(t *testing.T) {
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
var _ user.UserServiceServer = (*Server)(nil)
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) {
|
||||
|
@ -12,9 +12,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RegisterTOTP(t *testing.T) {
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func Test_totpDetailsToPb(t *testing.T) {
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) {
|
||||
@ -40,3 +40,13 @@ func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FR
|
||||
Details: object.DomainToDetailsPb(objectDetails),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RemoveU2F(ctx context.Context, req *user.RemoveU2FRequest) (*user.RemoveU2FResponse, error) {
|
||||
objectDetails, err := s.command.HumanRemoveU2F(ctx, req.GetUserId(), req.GetU2FId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RemoveU2FResponse{
|
||||
Details: object.DomainToDetailsPb(objectDetails),
|
||||
}, nil
|
||||
}
|
||||
|
@ -11,9 +11,10 @@ import (
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RegisterU2F(t *testing.T) {
|
||||
@ -106,16 +107,7 @@ func TestServer_RegisterU2F(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
|
||||
pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
|
||||
ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t)
|
||||
|
||||
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
@ -188,3 +180,138 @@ func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ctxFromNewUserWithRegisteredU2F(t *testing.T) (context.Context, string, *user.RegisterU2FResponse) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
ctx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||
|
||||
pkr, err := Client.RegisterU2F(ctx, &user.RegisterU2FRequest{
|
||||
UserId: userID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
|
||||
return ctx, userID, pkr
|
||||
}
|
||||
|
||||
func ctxFromNewUserWithVerifiedU2F(t *testing.T) (context.Context, string, string) {
|
||||
ctx, userID, pkr := ctxFromNewUserWithRegisteredU2F(t)
|
||||
|
||||
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Client.VerifyU2FRegistration(ctx, &user.VerifyU2FRegistrationRequest{
|
||||
UserId: userID,
|
||||
U2FId: pkr.GetU2FId(),
|
||||
PublicKeyCredential: attestationResponse,
|
||||
TokenName: "nice name",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return ctx, userID, pkr.GetU2FId()
|
||||
}
|
||||
|
||||
func TestServer_RemoveU2F(t *testing.T) {
|
||||
userIDWithout := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
ctxRegistered, userIDRegistered, pkrRegistered := ctxFromNewUserWithRegisteredU2F(t)
|
||||
_, userIDVerified, u2fVerified := ctxFromNewUserWithVerifiedU2F(t)
|
||||
_, userIDVerifiedPermission, u2fVerifiedPermission := ctxFromNewUserWithVerifiedU2F(t)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RemoveU2FRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RemoveU2FResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: ctxRegistered,
|
||||
req: &user.RemoveU2FRequest{
|
||||
U2FId: "123",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing u2f id",
|
||||
args: args{
|
||||
ctx: ctxRegistered,
|
||||
req: &user.RemoveU2FRequest{
|
||||
UserId: "123",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success, registered",
|
||||
args: args{
|
||||
ctx: ctxRegistered,
|
||||
req: &user.RemoveU2FRequest{
|
||||
UserId: userIDRegistered,
|
||||
U2FId: pkrRegistered.GetU2FId(),
|
||||
},
|
||||
},
|
||||
want: &user.RemoveU2FResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no u2f, error",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &user.RemoveU2FRequest{
|
||||
UserId: userIDWithout,
|
||||
U2FId: pkrRegistered.GetU2FId(),
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success, IAMOwner permission, verified",
|
||||
args: args{
|
||||
ctx: IamCTX,
|
||||
req: &user.RemoveU2FRequest{
|
||||
UserId: userIDVerified,
|
||||
U2FId: u2fVerified,
|
||||
},
|
||||
},
|
||||
want: &user.RemoveU2FResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verified, permission error",
|
||||
args: args{
|
||||
ctx: UserCTX,
|
||||
req: &user.RemoveU2FRequest{
|
||||
UserId: userIDVerifiedPermission,
|
||||
U2FId: u2fVerifiedPermission,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RemoveU2F(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||
|
@ -18,11 +18,12 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
||||
|
||||
human, err := AddUserRequestToAddHuman(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -259,20 +260,6 @@ func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
|
||||
details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{
|
||||
IDPID: req.GetIdpLink().GetIdpId(),
|
||||
DisplayName: req.GetIdpLink().GetUserName(),
|
||||
IDPExternalID: req.GetIdpLink().GetUserId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddIDPLinkResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) {
|
||||
memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId())
|
||||
if err != nil {
|
||||
|
@ -18,12 +18,13 @@ import (
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/idp"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -1797,86 +1798,6 @@ func TestServer_DeleteUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_AddIDPLink(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddIDPLinkRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.AddIDPLinkResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "user does not exist",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: "userID",
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
UserId: "userID",
|
||||
UserName: "username",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "idp does not exist",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: "idpID",
|
||||
UserId: "userID",
|
||||
UserName: "username",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add link",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID,
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
UserId: "userID",
|
||||
UserName: "username",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.AddIDPLinkResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_StartIdentityProviderIntent(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t, CTX)
|
||||
orgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, Tester.Organisation.ID)
|
||||
|
@ -17,8 +17,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
|
||||
func Test_idpIntentToIDPIntentPb(t *testing.T) {
|
||||
|
86
internal/api/grpc/user/v2beta/email.go
Normal file
86
internal/api/grpc/user/v2beta/email.go
Normal file
@ -0,0 +1,86 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||
var email *domain.Email
|
||||
|
||||
switch v := req.GetVerification().(type) {
|
||||
case *user.SetEmailRequest_SendCode:
|
||||
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
|
||||
case *user.SetEmailRequest_ReturnCode:
|
||||
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg)
|
||||
case *user.SetEmailRequest_IsVerified:
|
||||
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), req.GetEmail())
|
||||
case nil:
|
||||
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg)
|
||||
default:
|
||||
err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.SetEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: email.Sequence,
|
||||
ChangeDate: timestamppb.New(email.ChangeDate),
|
||||
ResourceOwner: email.ResourceOwner,
|
||||
},
|
||||
VerificationCode: email.PlainCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.ResendEmailCodeResponse, err error) {
|
||||
var email *domain.Email
|
||||
|
||||
switch v := req.GetVerification().(type) {
|
||||
case *user.ResendEmailCodeRequest_SendCode:
|
||||
email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
|
||||
case *user.ResendEmailCodeRequest_ReturnCode:
|
||||
email, err = s.command.ResendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg)
|
||||
case nil:
|
||||
email, err = s.command.ResendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg)
|
||||
default:
|
||||
err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method ResendEmailCode not implemented", v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.ResendEmailCodeResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: email.Sequence,
|
||||
ChangeDate: timestamppb.New(email.ChangeDate),
|
||||
ResourceOwner: email.ResourceOwner,
|
||||
},
|
||||
VerificationCode: email.PlainCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) {
|
||||
details, err := s.command.VerifyUserEmail(ctx,
|
||||
req.GetUserId(),
|
||||
req.GetVerificationCode(),
|
||||
s.userCodeAlg,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.VerifyEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: details.Sequence,
|
||||
ChangeDate: timestamppb.New(details.EventDate),
|
||||
ResourceOwner: details.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
297
internal/api/grpc/user/v2beta/email_integration_test.go
Normal file
297
internal/api/grpc/user/v2beta/email_integration_test.go
Normal file
@ -0,0 +1,297 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_SetEmail(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *user.SetEmailRequest
|
||||
want *user.SetEmailResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "user not existing",
|
||||
req: &user.SetEmailRequest{
|
||||
UserId: "xxx",
|
||||
Email: "default-verifier@mouse.com",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "default verfication",
|
||||
req: &user.SetEmailRequest{
|
||||
UserId: userID,
|
||||
Email: "default-verifier@mouse.com",
|
||||
},
|
||||
want: &user.SetEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom url template",
|
||||
req: &user.SetEmailRequest{
|
||||
UserId: userID,
|
||||
Email: "custom-url@mouse.com",
|
||||
Verification: &user.SetEmailRequest_SendCode{
|
||||
SendCode: &user.SendEmailVerificationCode{
|
||||
UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.SetEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template error",
|
||||
req: &user.SetEmailRequest{
|
||||
UserId: userID,
|
||||
Email: "custom-url@mouse.com",
|
||||
Verification: &user.SetEmailRequest_SendCode{
|
||||
SendCode: &user.SendEmailVerificationCode{
|
||||
UrlTemplate: gu.Ptr("{{"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "return code",
|
||||
req: &user.SetEmailRequest{
|
||||
UserId: userID,
|
||||
Email: "return-code@mouse.com",
|
||||
Verification: &user.SetEmailRequest_ReturnCode{
|
||||
ReturnCode: &user.ReturnEmailVerificationCode{},
|
||||
},
|
||||
},
|
||||
want: &user.SetEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
VerificationCode: gu.Ptr("xxx"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is verified true",
|
||||
req: &user.SetEmailRequest{
|
||||
UserId: userID,
|
||||
Email: "verified-true@mouse.com",
|
||||
Verification: &user.SetEmailRequest_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
want: &user.SetEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is verified false",
|
||||
req: &user.SetEmailRequest{
|
||||
UserId: userID,
|
||||
Email: "verified-false@mouse.com",
|
||||
Verification: &user.SetEmailRequest_IsVerified{
|
||||
IsVerified: false,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.SetEmail(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want.GetVerificationCode() != "" {
|
||||
assert.NotEmpty(t, got.GetVerificationCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResendEmailCode(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *user.ResendEmailCodeRequest
|
||||
want *user.ResendEmailCodeResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "user not existing",
|
||||
req: &user.ResendEmailCodeRequest{
|
||||
UserId: "xxx",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user no code",
|
||||
req: &user.ResendEmailCodeRequest{
|
||||
UserId: verifiedUserID,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "resend",
|
||||
req: &user.ResendEmailCodeRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
want: &user.ResendEmailCodeResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom url template",
|
||||
req: &user.ResendEmailCodeRequest{
|
||||
UserId: userID,
|
||||
Verification: &user.ResendEmailCodeRequest_SendCode{
|
||||
SendCode: &user.SendEmailVerificationCode{
|
||||
UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.ResendEmailCodeResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template error",
|
||||
req: &user.ResendEmailCodeRequest{
|
||||
UserId: userID,
|
||||
Verification: &user.ResendEmailCodeRequest_SendCode{
|
||||
SendCode: &user.SendEmailVerificationCode{
|
||||
UrlTemplate: gu.Ptr("{{"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "return code",
|
||||
req: &user.ResendEmailCodeRequest{
|
||||
UserId: userID,
|
||||
Verification: &user.ResendEmailCodeRequest_ReturnCode{
|
||||
ReturnCode: &user.ReturnEmailVerificationCode{},
|
||||
},
|
||||
},
|
||||
want: &user.ResendEmailCodeResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
VerificationCode: gu.Ptr("xxx"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResendEmailCode(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want.GetVerificationCode() != "" {
|
||||
assert.NotEmpty(t, got.GetVerificationCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_VerifyEmail(t *testing.T) {
|
||||
userResp := Tester.CreateHumanUser(CTX)
|
||||
tests := []struct {
|
||||
name string
|
||||
req *user.VerifyEmailRequest
|
||||
want *user.VerifyEmailResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "wrong code",
|
||||
req: &user.VerifyEmailRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
VerificationCode: "xxx",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong user",
|
||||
req: &user.VerifyEmailRequest{
|
||||
UserId: "xxx",
|
||||
VerificationCode: userResp.GetEmailCode(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify user",
|
||||
req: &user.VerifyEmailRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
VerificationCode: userResp.GetEmailCode(),
|
||||
},
|
||||
want: &user.VerifyEmailResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.VerifyEmail(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
42
internal/api/grpc/user/v2beta/otp.go
Normal file
42
internal/api/grpc/user/v2beta/otp.go
Normal file
@ -0,0 +1,42 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) {
|
||||
details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) {
|
||||
objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) {
|
||||
details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) {
|
||||
objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
|
||||
}
|
362
internal/api/grpc/user/v2beta/otp_integration_test.go
Normal file
362
internal/api/grpc/user/v2beta/otp_integration_test.go
Normal file
@ -0,0 +1,362 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_AddOTPSMS(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
|
||||
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, otherUser)
|
||||
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
|
||||
|
||||
userVerified := Tester.CreateHumanUser(CTX)
|
||||
_, err := Client.VerifyPhone(CTX, &user.VerifyPhoneRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
VerificationCode: userVerified.GetPhoneCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
|
||||
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
|
||||
|
||||
userVerified2 := Tester.CreateHumanUser(CTX)
|
||||
_, err = Client.VerifyPhone(CTX, &user.VerifyPhoneRequest{
|
||||
UserId: userVerified2.GetUserId(),
|
||||
VerificationCode: userVerified2.GetPhoneCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddOTPSMSRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.AddOTPSMSResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPSMSRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser),
|
||||
req: &user.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "phone not verified",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.AddOTPSMSRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add success",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified),
|
||||
req: &user.AddOTPSMSRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.AddOTPSMSResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add success, admin",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPSMSRequest{
|
||||
UserId: userVerified2.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.AddOTPSMSResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddOTPSMS(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RemoveOTPSMS(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
|
||||
userVerified := Tester.CreateHumanUser(CTX)
|
||||
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
|
||||
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
|
||||
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
|
||||
_, err := Client.VerifyPhone(userVerifiedCtx, &user.VerifyPhoneRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
VerificationCode: userVerified.GetPhoneCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = Client.AddOTPSMS(userVerifiedCtx, &user.AddOTPSMSRequest{UserId: userVerified.GetUserId()})
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RemoveOTPSMSRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RemoveOTPSMSResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "not added",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.RemoveOTPSMSRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: userVerifiedCtx,
|
||||
req: &user.RemoveOTPSMSRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.RemoveOTPSMSResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RemoveOTPSMS(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_AddOTPEmail(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
|
||||
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, otherUser)
|
||||
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
|
||||
|
||||
userVerified := Tester.CreateHumanUser(CTX)
|
||||
_, err := Client.VerifyEmail(CTX, &user.VerifyEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
VerificationCode: userVerified.GetEmailCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
|
||||
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
|
||||
|
||||
userVerified2 := Tester.CreateHumanUser(CTX)
|
||||
_, err = Client.VerifyEmail(CTX, &user.VerifyEmailRequest{
|
||||
UserId: userVerified2.GetUserId(),
|
||||
VerificationCode: userVerified2.GetEmailCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddOTPEmailRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.AddOTPEmailResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPEmailRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser),
|
||||
req: &user.AddOTPEmailRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "email not verified",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.AddOTPEmailRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add success",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified),
|
||||
req: &user.AddOTPEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.AddOTPEmailResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add success, admin",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.AddOTPEmailRequest{
|
||||
UserId: userVerified2.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.AddOTPEmailResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddOTPEmail(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RemoveOTPEmail(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
|
||||
userVerified := Tester.CreateHumanUser(CTX)
|
||||
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
|
||||
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
|
||||
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
|
||||
_, err := Client.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
VerificationCode: userVerified.GetEmailCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = Client.AddOTPEmail(userVerifiedCtx, &user.AddOTPEmailRequest{UserId: userVerified.GetUserId()})
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RemoveOTPEmailRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RemoveOTPEmailResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "not added",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
||||
req: &user.RemoveOTPEmailRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: userVerifiedCtx,
|
||||
req: &user.RemoveOTPEmailRequest{
|
||||
UserId: userVerified.GetUserId(),
|
||||
},
|
||||
},
|
||||
want: &user.RemoveOTPEmailResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ResourceOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RemoveOTPEmail(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
118
internal/api/grpc/user/v2beta/passkey.go
Normal file
118
internal/api/grpc/user/v2beta/passkey.go
Normal file
@ -0,0 +1,118 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
|
||||
var (
|
||||
authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator())
|
||||
)
|
||||
if code := req.GetCode(); code != nil {
|
||||
return passkeyRegistrationDetailsToPb(
|
||||
s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), "", authenticator, code.Id, code.Code, req.GetDomain(), s.userCodeAlg),
|
||||
)
|
||||
}
|
||||
return passkeyRegistrationDetailsToPb(
|
||||
s.command.RegisterUserPasskey(ctx, req.GetUserId(), "", req.GetDomain(), authenticator),
|
||||
)
|
||||
}
|
||||
|
||||
func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.AuthenticatorAttachment {
|
||||
switch pa {
|
||||
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED:
|
||||
return domain.AuthenticatorAttachmentUnspecified
|
||||
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM:
|
||||
return domain.AuthenticatorAttachmentPlattform
|
||||
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM:
|
||||
return domain.AuthenticatorAttachmentCrossPlattform
|
||||
default:
|
||||
return domain.AuthenticatorAttachmentUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*object_pb.Details, *structpb.Struct, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
options := new(structpb.Struct)
|
||||
if err := options.UnmarshalJSON(details.PublicKeyCredentialCreationOptions); err != nil {
|
||||
return nil, nil, zerrors.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal")
|
||||
}
|
||||
return object.DomainToDetailsPb(details.ObjectDetails), options, nil
|
||||
}
|
||||
|
||||
func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
|
||||
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RegisterPasskeyResponse{
|
||||
Details: objectDetails,
|
||||
PasskeyId: details.ID,
|
||||
PublicKeyCredentialCreationOptions: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
|
||||
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
|
||||
}
|
||||
objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), "", req.GetPasskeyName(), "", pkc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.VerifyPasskeyRegistrationResponse{
|
||||
Details: object.DomainToDetailsPb(objectDetails),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) {
|
||||
switch medium := req.Medium.(type) {
|
||||
case nil:
|
||||
return passkeyDetailsToPb(
|
||||
s.command.AddUserPasskeyCode(ctx, req.GetUserId(), "", s.userCodeAlg),
|
||||
)
|
||||
case *user.CreatePasskeyRegistrationLinkRequest_SendLink:
|
||||
return passkeyDetailsToPb(
|
||||
s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()),
|
||||
)
|
||||
case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode:
|
||||
return passkeyCodeDetailsToPb(
|
||||
s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), "", s.userCodeAlg),
|
||||
)
|
||||
default:
|
||||
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium)
|
||||
}
|
||||
}
|
||||
|
||||
func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: object.DomainToDetailsPb(details.ObjectDetails),
|
||||
Code: &user.PasskeyRegistrationCode{
|
||||
Id: details.CodeID,
|
||||
Code: details.Code,
|
||||
},
|
||||
}, nil
|
||||
}
|
319
internal/api/grpc/user/v2beta/passkey_integration_test.go
Normal file
319
internal/api/grpc/user/v2beta/passkey_integration_test.go
Normal file
@ -0,0 +1,319 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RegisterPasskey(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// We also need a user session
|
||||
Tester.RegisterUserPasskey(CTX, userID)
|
||||
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RegisterPasskeyRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RegisterPasskeyResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "register code",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
|
||||
},
|
||||
},
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reuse code (not allowed)",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong code",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: &user.PasskeyRegistrationCode{
|
||||
Id: reg.GetCode().GetId(),
|
||||
Code: "foobar",
|
||||
},
|
||||
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user setting its own passkey",
|
||||
args: args{
|
||||
ctx: Tester.WithAuthorizationToken(CTX, sessionToken),
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RegisterPasskey(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want != nil {
|
||||
assert.NotEmpty(t, got.GetPasskeyId())
|
||||
assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions())
|
||||
_, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkr.GetPasskeyId())
|
||||
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
|
||||
|
||||
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.VerifyPasskeyRegistrationRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.VerifyPasskeyRegistrationResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.VerifyPasskeyRegistrationRequest{
|
||||
PasskeyId: pkr.GetPasskeyId(),
|
||||
PublicKeyCredential: attestationResponse,
|
||||
PasskeyName: "nice name",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.VerifyPasskeyRegistrationRequest{
|
||||
UserId: userID,
|
||||
PasskeyId: pkr.GetPasskeyId(),
|
||||
PublicKeyCredential: attestationResponse,
|
||||
PasskeyName: "nice name",
|
||||
},
|
||||
},
|
||||
want: &user.VerifyPasskeyRegistrationResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong credential",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.VerifyPasskeyRegistrationRequest{
|
||||
UserId: userID,
|
||||
PasskeyId: pkr.GetPasskeyId(),
|
||||
PublicKeyCredential: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}},
|
||||
},
|
||||
PasskeyName: "nice name",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.VerifyPasskeyRegistration(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.CreatePasskeyRegistrationLinkRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.CreatePasskeyRegistrationLinkResponse
|
||||
wantCode bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "send default mail",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send custom url",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_SendLink{
|
||||
SendLink: &user.SendPasskeyRegistrationLink{
|
||||
UrlTemplate: gu.Ptr("https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "return code",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
},
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
wantCode: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.CreatePasskeyRegistrationLink(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.wantCode {
|
||||
assert.NotEmpty(t, got.GetCode().GetId())
|
||||
assert.NotEmpty(t, got.GetCode().GetId())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
235
internal/api/grpc/user/v2beta/passkey_test.go
Normal file
235
internal/api/grpc/user/v2beta/passkey_test.go
Normal file
@ -0,0 +1,235 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
pa user.PasskeyAuthenticator
|
||||
want domain.AuthenticatorAttachment
|
||||
}{
|
||||
{
|
||||
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED,
|
||||
want: domain.AuthenticatorAttachmentUnspecified,
|
||||
},
|
||||
{
|
||||
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
|
||||
want: domain.AuthenticatorAttachmentPlattform,
|
||||
},
|
||||
{
|
||||
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
|
||||
want: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
},
|
||||
{
|
||||
pa: 999,
|
||||
want: domain.AuthenticatorAttachmentUnspecified,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.pa.String(), func(t *testing.T) {
|
||||
got := passkeyAuthenticatorToDomain(tt.pa)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
type args struct {
|
||||
details *domain.WebAuthNRegistrationDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RegisterPasskeyResponse
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "an error",
|
||||
args: args{
|
||||
details: nil,
|
||||
err: io.ErrClosedPipe,
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "unmarshall error",
|
||||
args: args{
|
||||
details: &domain.WebAuthNRegistrationDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte(`\\`),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
wantErr: zerrors.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
details: &domain.WebAuthNRegistrationDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
ID: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
69
internal/api/grpc/user/v2beta/password.go
Normal file
69
internal/api/grpc/user/v2beta/password.go
Normal file
@ -0,0 +1,69 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) {
|
||||
var details *domain.ObjectDetails
|
||||
var code *string
|
||||
|
||||
switch m := req.GetMedium().(type) {
|
||||
case *user.PasswordResetRequest_SendLink:
|
||||
details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType()))
|
||||
case *user.PasswordResetRequest_ReturnCode:
|
||||
details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.GetUserId())
|
||||
case nil:
|
||||
details, code, err = s.command.RequestPasswordReset(ctx, req.GetUserId())
|
||||
default:
|
||||
err = zerrors.ThrowUnimplementedf(nil, "USERv2-SDeeg", "verification oneOf %T in method RequestPasswordReset not implemented", m)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.PasswordResetResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
VerificationCode: code,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func notificationTypeToDomain(notificationType user.NotificationType) domain.NotificationType {
|
||||
switch notificationType {
|
||||
case user.NotificationType_NOTIFICATION_TYPE_Email:
|
||||
return domain.NotificationTypeEmail
|
||||
case user.NotificationType_NOTIFICATION_TYPE_SMS:
|
||||
return domain.NotificationTypeSms
|
||||
case user.NotificationType_NOTIFICATION_TYPE_Unspecified:
|
||||
return domain.NotificationTypeEmail
|
||||
default:
|
||||
return domain.NotificationTypeEmail
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) {
|
||||
var details *domain.ObjectDetails
|
||||
|
||||
switch v := req.GetVerification().(type) {
|
||||
case *user.SetPasswordRequest_CurrentPassword:
|
||||
details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
|
||||
case *user.SetPasswordRequest_VerificationCode:
|
||||
details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired())
|
||||
case nil:
|
||||
details, err = s.command.SetPassword(ctx, "", req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired())
|
||||
default:
|
||||
err = zerrors.ThrowUnimplementedf(nil, "USERv2-SFdf2", "verification oneOf %T in method SetPasswordRequest not implemented", v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.SetPasswordResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
232
internal/api/grpc/user/v2beta/password_integration_test.go
Normal file
232
internal/api/grpc/user/v2beta/password_integration_test.go
Normal file
@ -0,0 +1,232 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_RequestPasswordReset(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *user.PasswordResetRequest
|
||||
want *user.PasswordResetResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "default medium",
|
||||
req: &user.PasswordResetRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
want: &user.PasswordResetResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom url template",
|
||||
req: &user.PasswordResetRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.PasswordResetRequest_SendLink{
|
||||
SendLink: &user.SendPasswordResetLink{
|
||||
NotificationType: user.NotificationType_NOTIFICATION_TYPE_Email,
|
||||
UrlTemplate: gu.Ptr("https://example.com/password/change?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.PasswordResetResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template error",
|
||||
req: &user.PasswordResetRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.PasswordResetRequest_SendLink{
|
||||
SendLink: &user.SendPasswordResetLink{
|
||||
UrlTemplate: gu.Ptr("{{"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "return code",
|
||||
req: &user.PasswordResetRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.PasswordResetRequest_ReturnCode{
|
||||
ReturnCode: &user.ReturnPasswordResetCode{},
|
||||
},
|
||||
},
|
||||
want: &user.PasswordResetResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
VerificationCode: gu.Ptr("xxx"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.PasswordReset(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want.GetVerificationCode() != "" {
|
||||
assert.NotEmpty(t, got.GetVerificationCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_SetPassword(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.SetPasswordRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func(request *user.SetPasswordRequest) error
|
||||
args args
|
||||
want *user.SetPasswordResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
prepare: func(request *user.SetPasswordRequest) error {
|
||||
return nil
|
||||
},
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.SetPasswordRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "set successful",
|
||||
prepare: func(request *user.SetPasswordRequest) error {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
request.UserId = userID
|
||||
return nil
|
||||
},
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.SetPasswordRequest{
|
||||
NewPassword: &user.Password{
|
||||
Password: "Secr3tP4ssw0rd!",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.SetPasswordResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change successful",
|
||||
prepare: func(request *user.SetPasswordRequest) error {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
request.UserId = userID
|
||||
_, err := Client.SetPassword(CTX, &user.SetPasswordRequest{
|
||||
UserId: userID,
|
||||
NewPassword: &user.Password{
|
||||
Password: "InitialPassw0rd!",
|
||||
},
|
||||
})
|
||||
return err
|
||||
},
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.SetPasswordRequest{
|
||||
NewPassword: &user.Password{
|
||||
Password: "Secr3tP4ssw0rd!",
|
||||
},
|
||||
Verification: &user.SetPasswordRequest_CurrentPassword{
|
||||
CurrentPassword: "InitialPassw0rd!",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.SetPasswordResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set with code successful",
|
||||
prepare: func(request *user.SetPasswordRequest) error {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
request.UserId = userID
|
||||
resp, err := Client.PasswordReset(CTX, &user.PasswordResetRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.PasswordResetRequest_ReturnCode{
|
||||
ReturnCode: &user.ReturnPasswordResetCode{},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Verification = &user.SetPasswordRequest_VerificationCode{
|
||||
VerificationCode: resp.GetVerificationCode(),
|
||||
}
|
||||
return nil
|
||||
},
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.SetPasswordRequest{
|
||||
NewPassword: &user.Password{
|
||||
Password: "Secr3tP4ssw0rd!",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.SetPasswordResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.prepare(tt.args.req)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := Client.SetPassword(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
39
internal/api/grpc/user/v2beta/password_test.go
Normal file
39
internal/api/grpc/user/v2beta/password_test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func Test_notificationTypeToDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
notificationType user.NotificationType
|
||||
want domain.NotificationType
|
||||
}{
|
||||
{
|
||||
"unspecified",
|
||||
user.NotificationType_NOTIFICATION_TYPE_Unspecified,
|
||||
domain.NotificationTypeEmail,
|
||||
},
|
||||
{
|
||||
"email",
|
||||
user.NotificationType_NOTIFICATION_TYPE_Email,
|
||||
domain.NotificationTypeEmail,
|
||||
},
|
||||
{
|
||||
"sms",
|
||||
user.NotificationType_NOTIFICATION_TYPE_SMS,
|
||||
domain.NotificationTypeSms,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, notificationTypeToDomain(tt.notificationType), "notificationTypeToDomain(%v)", tt.notificationType)
|
||||
})
|
||||
}
|
||||
}
|
102
internal/api/grpc/user/v2beta/phone.go
Normal file
102
internal/api/grpc/user/v2beta/phone.go
Normal file
@ -0,0 +1,102 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) {
|
||||
var phone *domain.Phone
|
||||
|
||||
switch v := req.GetVerification().(type) {
|
||||
case *user.SetPhoneRequest_SendCode:
|
||||
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
|
||||
case *user.SetPhoneRequest_ReturnCode:
|
||||
phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
|
||||
case *user.SetPhoneRequest_IsVerified:
|
||||
phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), req.GetPhone())
|
||||
case nil:
|
||||
phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg)
|
||||
default:
|
||||
err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.SetPhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: phone.Sequence,
|
||||
ChangeDate: timestamppb.New(phone.ChangeDate),
|
||||
ResourceOwner: phone.ResourceOwner,
|
||||
},
|
||||
VerificationCode: phone.PlainCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) {
|
||||
details, err := s.command.RemoveUserPhone(ctx,
|
||||
req.GetUserId(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.RemovePhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: details.Sequence,
|
||||
ChangeDate: timestamppb.New(details.EventDate),
|
||||
ResourceOwner: details.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) {
|
||||
var phone *domain.Phone
|
||||
switch v := req.GetVerification().(type) {
|
||||
case *user.ResendPhoneCodeRequest_SendCode:
|
||||
phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg)
|
||||
case *user.ResendPhoneCodeRequest_ReturnCode:
|
||||
phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.GetUserId(), s.userCodeAlg)
|
||||
case nil:
|
||||
phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg)
|
||||
default:
|
||||
err = zerrors.ThrowUnimplementedf(nil, "USERv2-ResendUserPhoneCode", "verification oneOf %T in method SetPhone not implemented", v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user.ResendPhoneCodeResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: phone.Sequence,
|
||||
ChangeDate: timestamppb.New(phone.ChangeDate),
|
||||
ResourceOwner: phone.ResourceOwner,
|
||||
},
|
||||
VerificationCode: phone.PlainCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) {
|
||||
details, err := s.command.VerifyUserPhone(ctx,
|
||||
req.GetUserId(),
|
||||
req.GetVerificationCode(),
|
||||
s.userCodeAlg,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.VerifyPhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: details.Sequence,
|
||||
ChangeDate: timestamppb.New(details.EventDate),
|
||||
ResourceOwner: details.ResourceOwner,
|
||||
},
|
||||
}, nil
|
||||
}
|
344
internal/api/grpc/user/v2beta/phone_integration_test.go
Normal file
344
internal/api/grpc/user/v2beta/phone_integration_test.go
Normal file
@ -0,0 +1,344 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_SetPhone(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *user.SetPhoneRequest
|
||||
want *user.SetPhoneResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "default verification",
|
||||
req: &user.SetPhoneRequest{
|
||||
UserId: userID,
|
||||
Phone: "+41791234568",
|
||||
},
|
||||
want: &user.SetPhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send verification",
|
||||
req: &user.SetPhoneRequest{
|
||||
UserId: userID,
|
||||
Phone: "+41791234569",
|
||||
Verification: &user.SetPhoneRequest_SendCode{
|
||||
SendCode: &user.SendPhoneVerificationCode{},
|
||||
},
|
||||
},
|
||||
want: &user.SetPhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "return code",
|
||||
req: &user.SetPhoneRequest{
|
||||
UserId: userID,
|
||||
Phone: "+41791234566",
|
||||
Verification: &user.SetPhoneRequest_ReturnCode{
|
||||
ReturnCode: &user.ReturnPhoneVerificationCode{},
|
||||
},
|
||||
},
|
||||
want: &user.SetPhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
VerificationCode: gu.Ptr("xxx"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is verified true",
|
||||
req: &user.SetPhoneRequest{
|
||||
UserId: userID,
|
||||
Phone: "+41791234565",
|
||||
Verification: &user.SetPhoneRequest_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
want: &user.SetPhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is verified false",
|
||||
req: &user.SetPhoneRequest{
|
||||
UserId: userID,
|
||||
Phone: "+41791234564",
|
||||
Verification: &user.SetPhoneRequest_IsVerified{
|
||||
IsVerified: false,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.SetPhone(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want.GetVerificationCode() != "" {
|
||||
assert.NotEmpty(t, got.GetVerificationCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ResendPhoneCode(t *testing.T) {
|
||||
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
verifiedUserID := Tester.CreateHumanUserVerified(CTX, Tester.Organisation.ID, fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())).GetUserId()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *user.ResendPhoneCodeRequest
|
||||
want *user.ResendPhoneCodeResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "user not existing",
|
||||
req: &user.ResendPhoneCodeRequest{
|
||||
UserId: "xxx",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user not existing",
|
||||
req: &user.ResendPhoneCodeRequest{
|
||||
UserId: verifiedUserID,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "resend code",
|
||||
req: &user.ResendPhoneCodeRequest{
|
||||
UserId: userID,
|
||||
Verification: &user.ResendPhoneCodeRequest_SendCode{
|
||||
SendCode: &user.SendPhoneVerificationCode{},
|
||||
},
|
||||
},
|
||||
want: &user.ResendPhoneCodeResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "return code",
|
||||
req: &user.ResendPhoneCodeRequest{
|
||||
UserId: userID,
|
||||
Verification: &user.ResendPhoneCodeRequest_ReturnCode{
|
||||
ReturnCode: &user.ReturnPhoneVerificationCode{},
|
||||
},
|
||||
},
|
||||
want: &user.ResendPhoneCodeResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
VerificationCode: gu.Ptr("xxx"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.ResendPhoneCode(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want.GetVerificationCode() != "" {
|
||||
assert.NotEmpty(t, got.GetVerificationCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_VerifyPhone(t *testing.T) {
|
||||
userResp := Tester.CreateHumanUser(CTX)
|
||||
tests := []struct {
|
||||
name string
|
||||
req *user.VerifyPhoneRequest
|
||||
want *user.VerifyPhoneResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "wrong code",
|
||||
req: &user.VerifyPhoneRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
VerificationCode: "xxx",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong user",
|
||||
req: &user.VerifyPhoneRequest{
|
||||
UserId: "xxx",
|
||||
VerificationCode: userResp.GetPhoneCode(),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify user",
|
||||
req: &user.VerifyPhoneRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
VerificationCode: userResp.GetPhoneCode(),
|
||||
},
|
||||
want: &user.VerifyPhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.VerifyPhone(CTX, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RemovePhone(t *testing.T) {
|
||||
userResp := Tester.CreateHumanUser(CTX)
|
||||
failResp := Tester.CreateHumanUserNoPhone(CTX)
|
||||
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
|
||||
doubleRemoveUser := Tester.CreateHumanUser(CTX)
|
||||
|
||||
Tester.RegisterUserPasskey(CTX, otherUser)
|
||||
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
req *user.RemovePhoneRequest
|
||||
want *user.RemovePhoneResponse
|
||||
wantErr bool
|
||||
dep func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "remove phone",
|
||||
ctx: CTX,
|
||||
req: &user.RemovePhoneRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
},
|
||||
want: &user.RemovePhoneResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 1,
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user without phone",
|
||||
ctx: CTX,
|
||||
req: &user.RemovePhoneRequest{
|
||||
UserId: failResp.GetUserId(),
|
||||
},
|
||||
wantErr: true,
|
||||
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove previously deleted phone",
|
||||
ctx: CTX,
|
||||
req: &user.RemovePhoneRequest{
|
||||
UserId: doubleRemoveUser.GetUserId(),
|
||||
},
|
||||
wantErr: true,
|
||||
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||
return Client.RemovePhone(ctx, &user.RemovePhoneRequest{
|
||||
UserId: doubleRemoveUser.GetUserId(),
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no user id",
|
||||
ctx: CTX,
|
||||
req: &user.RemovePhoneRequest{},
|
||||
wantErr: true,
|
||||
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "other user, no permission",
|
||||
ctx: Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser),
|
||||
req: &user.RemovePhoneRequest{
|
||||
UserId: userResp.GetUserId(),
|
||||
},
|
||||
wantErr: true,
|
||||
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, depErr := tt.dep(tt.ctx, tt.req.UserId)
|
||||
require.NoError(t, depErr)
|
||||
|
||||
got, err := Client.RemovePhone(tt.ctx, tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user