feat(rt): IDP Intents Relational Table (#11374)

# Which Problems Are Solved

In the context of moving our datastore to relational tables, Identity
Provider Intents have no definition of a relational table nor any DB API
that can be used to store the IDP intents as relational objects.

# How the Problems Are Solved

The PR follows the guidelines left in #11190

- A table `zitadel.identity_provider_intents` is created: it defines its
relational constraints (foreign keys) with the other tables, as well as
an ENUM to represent the state of the intent.
- An IDP Intent repository has been created, which offers CRUD
operations implemented as `Create`, `Get`, `Update` and `Delete` methods
for the IDP intent.
- The repository also defines all columns and conditions useful for
performing DB operations on the IDP intents.
- DB mocks have been added as well to ease testing on the `domain`
package.
- Reduction functions have been implemented for the existing IDP events
so to keep the relational table in sync with the event store.

# Additional Changes

To allow testing LDAP intent flow, the `Session.FetchUser()` method has
been extracted to a separate file. A corresponding mock has been created
as well in `fetch_user_mock.go`. The files are mutually excluded and
added at build time through go build tags: the mock is added when the
`integration` tag is specified, while the real implementation is added
when the same tag IS NOT added to the build tags.

# Additional Context

- Closes #11190

---------

Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Silvan <[email protected]>
This commit is contained in:
Marco A.
2026-02-05 03:44:07 +00:00
committed by GitHub
co-authored by Livio Spring Silvan
parent ca9c7619c6
commit 4936bdf2c9
23 changed files with 4570 additions and 471 deletions
+174
View File
@@ -0,0 +1,174 @@
package domain
import (
"context"
"net/url"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
//go:generate enumer -type IDPIntentState -transform lower -trimprefix IDPIntentState -sql
type IDPIntentState uint8
const (
IDPIntentStateStarted IDPIntentState = iota
IDPIntentStateSucceeded
IDPIntentStateFailed
)
type IDPIntent struct {
ID string `json:"id,omitempty" db:"id"`
InstanceID string `json:"instance_id,omitempty" db:"instance_id"`
State IDPIntentState `json:"state" db:"state"`
SuccessURL *url.URL `json:"success_url,omitempty" db:"success_url"`
FailureURL *url.URL `json:"failure_url,omitempty" db:"failure_url"`
CreatedAt time.Time `json:"created_at,omitzero" db:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitzero" db:"updated_at"`
IDPID string `json:"idp_id,omitempty" db:"idp_id"`
IDPArguments map[string]any `json:"idp_arguments,omitempty" db:"idp_arguments"`
IDPUser []byte `json:"idp_user,omitempty" db:"idp_user"`
IDPUserID string `json:"idp_user_id,omitempty" db:"idp_user_id"`
IDPUsername string `json:"idp_username,omitempty" db:"idp_username"`
UserID string `json:"user_id,omitempty" db:"user_id"`
IDPAccessToken []byte `json:"idp_access_token,omitempty" db:"idp_access_token"`
IDPIDToken string `json:"idp_id_token,omitempty" db:"idp_id_token"`
EntryAttributes map[string][]string `json:"idp_entry_attributes,omitempty" db:"idp_entry_attributes"`
RequestID string `json:"request_id,omitempty" db:"request_id"`
Assertion []byte `json:"assertion,omitempty" db:"assertion"`
SucceededAt *time.Time `json:"succeeded_at,omitzero" db:"succeeded_at"`
FailReason string `json:"fail_reason,omitempty" db:"fail_reason"`
FailedAt *time.Time `json:"failed_at,omitzero" db:"failed_at"`
ExpiresAt *time.Time `json:"expires_at,omitzero" db:"expires_at"`
}
// idpIntentColumns defines all the columns of the identity_provider_intents table.
type idpIntentColumns interface {
// IDColumn returns the column for the field id
IDColumn() database.Column
// InstanceIDColumn returns the column for the field instance_id
InstanceIDColumn() database.Column
// StateColumn returns the column for the field state
StateColumn() database.Column
// SuccessURLColumn returns the column for the field success_url
SuccessURLColumn() database.Column
// FailureURLColumn returns the column for the field failure_url
FailureURLColumn() database.Column
// CreatedAtColumn returns the column for the field created_at
CreatedAtColumn() database.Column
// UpdatedAtColumn returns the column for the field updated_at
UpdatedAtColumn() database.Column
// IDPIDColumn returns the column for the field idp_id
IDPIDColumn() database.Column
// IDPArgumentsColumn returns the column for the field idp_arguments
IDPArgumentsColumn() database.Column
// IDPUserColumn returns the column for the field idp_user
IDPUserColumn() database.Column
// IDPUserIDColumn returns the column for the field idp_user_id
IDPUserIDColumn() database.Column
// IDPUsernameColumn returns the column for the field idp_username
IDPUsernameColumn() database.Column
// UserIDColumn returns the column for the field user_id
UserIDColumn() database.Column
// IDPAccessTokenColumn returns the column for the field idp_access_token
IDPAccessTokenColumn() database.Column
// IDPIDTokenColumn returns the column for the field idp_id_token
IDPIDTokenColumn() database.Column
// IDPEntryAttributesColumn returns the column for the field idp_entry_attributes
IDPEntryAttributesColumn() database.Column
// RequestIDColumn returns the column for the field request_id
RequestIDColumn() database.Column
// AssertionColumn returns the column for the field assertion
AssertionColumn() database.Column
// SucceededAtColumn returns the column for the field succeeded_at
SucceededAtColumn() database.Column
// FailReasonColumn returns the column for the field fail_reason
FailReasonColumn() database.Column
// FailedAtColumn returns the column for the field failed_at
FailedAtColumn() database.Column
// ExpiresAtColumn returns the column for the field expires_at
ExpiresAtColumn() database.Column
}
type idpIntentConditions interface {
// PrimaryKeyCondition returns a filter on the primary key fields.
PrimaryKeyCondition(instanceID, id string) database.Condition
// IDCondition returns an equal filter on the ID
IDCondition(id string) database.Condition
// InstanceIDCondition returns an equal filter on the InstanceID
InstanceIDCondition(instanceID string) database.Condition
// StateCondition returns an equal filter on the State
StateCondition(state IDPIntentState) database.Condition
// IDPIDCondition returns an equal filter on the IDPID
IDPIDCondition(idpID string) database.Condition
// IDPUserIDCondition returns an equal filter on the IDPUserID
IDPUserIDCondition(idpUserID string) database.Condition
// IDPUsernameCondition returns an equal filter on the IDPUsername
IDPUsernameCondition(idpUsername string) database.Condition
// UserIDCondition returns an equal filter on the UserID
UserIDCondition(userID string) database.Condition
// RequestIDCondition returns an equal filter on the RequestID
RequestIDCondition(requestID string) database.Condition
// CreatedAtCondition returns a filter on the created at field.
CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition
// UpdatedAtCondition returns a filter on the updated at field.
UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition
}
type idpIntentChanges interface {
// SetState sets the state of the IDP intent
SetState(state IDPIntentState) database.Change
// SetSuccessURL sets the successURL of the IDP intent
SetSuccessURL(successURL url.URL) database.Change
// SetFailureURL sets the failureURL of the IDP intent
SetFailureURL(failureURL url.URL) database.Change
// SetIDPID sets the idpID of the IDP intent
SetIDPID(idpID string) database.Change
// SetIDPArguments sets the idpArguments of the IDP intent
SetIDPArguments(idpArguments []byte) database.Change
// SetIDPUser sets the idpUser of the IDP intent
SetIDPUser(idpUser []byte) database.Change
// SetIDPUserID sets the idpUserID of the IDP intent
SetIDPUserID(idpUserID string) database.Change
// SetIDPUsername sets the idpUsername of the IDP intent
SetIDPUsername(idpUsername string) database.Change
// SetUserID sets the userID of the IDP intent
SetUserID(userID string) database.Change
// SetIDPAccessToken sets the idpAccessToken of the IDP intent
SetIDPAccessToken(idpAccessToken []byte) database.Change
// SetIDPIDToken sets the idpIDToken of the IDP intent
SetIDPIDToken(idpIDToken string) database.Change
// SetIDPEntryAttributes sets the idpEntryAttributes of the IDP intent
SetIDPEntryAttributes(idpEntryAttributes []byte) database.Change
// SetRequestID sets the requestID of the IDP intent
SetRequestID(requestID string) database.Change
// SetAssertion sets the assertion of the IDP intent
SetAssertion(assertion []byte) database.Change
// SetFailReason sets the fail reason of the IDP intent
SetFailReason(reason string) database.Change
// SetExpiresAt sets the expiration of the IDP intent
SetExpiresAt(expiration time.Time) database.Change
// SetSucceededAt sets the succededAt time of the IDP intent
SetSucceededAt(succeededAt time.Time) database.Change
// SetFailedAt sets the failedAt time of the IDP intent
SetFailedAt(failedAt time.Time) database.Change
}
//go:generate mockgen -typed -package domainmock -destination ./mock/idp_intent.mock.go . IDPIntentRepository
type IDPIntentRepository interface {
Repository
idpIntentColumns
idpIntentConditions
idpIntentChanges
// Get returns an idp intent based on the given condition.
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*IDPIntent, error)
// Create creates a new idp intent.
Create(ctx context.Context, client database.QueryExecutor, intent *IDPIntent) error
// Update one or more existing idp intents.
// The condition must include at least the instanceID of the idp intent to update.
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
// Delete removes idp intents based on the given condition.
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
}
+113
View File
@@ -0,0 +1,113 @@
// Code generated by "enumer -type IDPIntentState -transform lower -trimprefix IDPIntentState -sql"; DO NOT EDIT.
package domain
import (
"database/sql/driver"
"fmt"
"strings"
)
const _IDPIntentStateName = "startedsucceededfailed"
var _IDPIntentStateIndex = [...]uint8{0, 7, 16, 22}
const _IDPIntentStateLowerName = "startedsucceededfailed"
func (i IDPIntentState) String() string {
if i >= IDPIntentState(len(_IDPIntentStateIndex)-1) {
return fmt.Sprintf("IDPIntentState(%d)", i)
}
return _IDPIntentStateName[_IDPIntentStateIndex[i]:_IDPIntentStateIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _IDPIntentStateNoOp() {
var x [1]struct{}
_ = x[IDPIntentStateStarted-(0)]
_ = x[IDPIntentStateSucceeded-(1)]
_ = x[IDPIntentStateFailed-(2)]
}
var _IDPIntentStateValues = []IDPIntentState{IDPIntentStateStarted, IDPIntentStateSucceeded, IDPIntentStateFailed}
var _IDPIntentStateNameToValueMap = map[string]IDPIntentState{
_IDPIntentStateName[0:7]: IDPIntentStateStarted,
_IDPIntentStateLowerName[0:7]: IDPIntentStateStarted,
_IDPIntentStateName[7:16]: IDPIntentStateSucceeded,
_IDPIntentStateLowerName[7:16]: IDPIntentStateSucceeded,
_IDPIntentStateName[16:22]: IDPIntentStateFailed,
_IDPIntentStateLowerName[16:22]: IDPIntentStateFailed,
}
var _IDPIntentStateNames = []string{
_IDPIntentStateName[0:7],
_IDPIntentStateName[7:16],
_IDPIntentStateName[16:22],
}
// IDPIntentStateString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func IDPIntentStateString(s string) (IDPIntentState, error) {
if val, ok := _IDPIntentStateNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _IDPIntentStateNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to IDPIntentState values", s)
}
// IDPIntentStateValues returns all values of the enum
func IDPIntentStateValues() []IDPIntentState {
return _IDPIntentStateValues
}
// IDPIntentStateStrings returns a slice of all String values of the enum
func IDPIntentStateStrings() []string {
strs := make([]string, len(_IDPIntentStateNames))
copy(strs, _IDPIntentStateNames)
return strs
}
// IsAIDPIntentState returns "true" if the value is listed in the enum definition. "false" otherwise
func (i IDPIntentState) IsAIDPIntentState() bool {
for _, v := range _IDPIntentStateValues {
if i == v {
return true
}
}
return false
}
func (i IDPIntentState) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *IDPIntentState) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of IDPIntentState: %[1]T(%[1]v)", value)
}
val, err := IDPIntentStateString(str)
if err != nil {
return err
}
*i = val
return nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
package migration
import (
_ "embed"
)
var (
//go:embed 012_identity_provider_intents_table/up.sql
up012IDPIntentsTable string
//go:embed 012_identity_provider_intents_table/down.sql
down012IDPIntentsTable string
)
func init() {
registerSQLMigration(12, up012IDPIntentsTable, down012IDPIntentsTable)
}
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS zitadel.identity_provider_intents;
DROP TYPE IF EXISTS zitadel.idp_intent_state;
@@ -0,0 +1,44 @@
CREATE TYPE zitadel.idp_intent_state AS ENUM (
'started',
'succeeded',
'failed',
'consumed'
);
CREATE TABLE zitadel.identity_provider_intents
(
instance_id TEXT NOT NULL,
id TEXT NOT NULL CHECK ( id <> '' ),
state zitadel.idp_intent_state NOT NULL DEFAULT 'started',
success_url TEXT,
failure_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
idp_id TEXT NOT NULL,
idp_arguments JSONB,
idp_user BYTEA,
idp_user_id TEXT,
idp_username TEXT,
user_id TEXT,
idp_access_token JSONB,
idp_id_token TEXT,
idp_entry_attributes JSONB,
request_id TEXT,
assertion JSONB,
succeeded_at TIMESTAMPTZ,
fail_reason TEXT,
failed_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
PRIMARY KEY (instance_id, id),
-- TODO(@adlerhurst): easiest would be to create the index in the user migrations so that we can merge this one but the problem is that the migration might fail if there are already intents created.
-- FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE,
FOREIGN KEY (instance_id, idp_id) REFERENCES zitadel.identity_providers (instance_id, id) ON DELETE CASCADE
);
CREATE TRIGGER trigger_set_updated_at
BEFORE UPDATE
ON zitadel.identity_provider_intents
FOR EACH ROW
WHEN (NEW.updated_at IS NULL)
EXECUTE FUNCTION zitadel.set_updated_at();
@@ -69,7 +69,7 @@ func TestMain(m *testing.M) {
CTX = integration.WithSystemAuthorization(ctx)
Instance = integration.NewInstance(CTX)
IAMCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
IAMCTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeIAMOwner)
SystemClient = integration.SystemClient()
OrgClient = Instance.Client.OrgV2beta
ProjectClient = Instance.Client.Projectv2Beta
@@ -145,17 +145,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
name := gofakeit.Name()
before := time.Now()
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: true,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, true))
after := time.Now()
require.NoError(t, err)
@@ -190,17 +180,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
t.Run("test iam idp update reduces", func(t *testing.T) {
name := gofakeit.Name()
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: true,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, true))
require.NoError(t, err)
name = "new_" + name
@@ -238,17 +218,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
t.Run("test iam idp deactivate reduces", func(t *testing.T) {
name := gofakeit.Name()
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: true,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, true))
require.NoError(t, err)
// deactivate idp
@@ -280,17 +250,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
t.Run("test iam idp config reactivate reduces", func(t *testing.T) {
name := gofakeit.Name()
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: true,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, true))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -342,17 +302,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
name := gofakeit.Name()
// add idp
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: true,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, true))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -380,17 +330,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
name := gofakeit.Name()
// add oidc
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: false,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, false))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -427,17 +367,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
name := gofakeit.Name()
// add oidc
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: true,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, true))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -1968,40 +1898,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
// add ldap
before := time.Now()
addLdap, err := AdminClient.AddLDAPProvider(IAMCTX, &admin.AddLDAPProviderRequest{
Name: name,
Servers: []string{"servers"},
StartTls: true,
BaseDn: "baseDN",
BindDn: "bindND",
BindPassword: "bindPassword",
UserBase: "userBase",
UserObjectClasses: []string{"userOhjectClasses"},
UserFilters: []string{"userFilters"},
Timeout: durationpb.New(time.Minute),
Attributes: &idp_grpc.LDAPAttributes{
IdAttribute: "idAttribute",
FirstNameAttribute: "firstNameAttribute",
LastNameAttribute: "lastNameAttribute",
DisplayNameAttribute: "displayNameAttribute",
NickNameAttribute: "nickNameAttribute",
PreferredUsernameAttribute: "preferredUsernameAttribute",
EmailAttribute: "emailAttribute",
EmailVerifiedAttribute: "emailVerifiedAttribute",
PhoneAttribute: "phoneAttribute",
PhoneVerifiedAttribute: "phoneVerifiedAttribute",
PreferredLanguageAttribute: "preferredLanguageAttribute",
AvatarUrlAttribute: "avatarUrlAttribute",
ProfileAttribute: "profileAttribute",
},
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
})
addLdap, err := AdminClient.AddLDAPProvider(IAMCTX, defaultInstanceLDAPRequest(name))
after := time.Now()
require.NoError(t, err)
@@ -2056,40 +1953,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
name := gofakeit.Name()
// add ldap
addLdap, err := AdminClient.AddLDAPProvider(IAMCTX, &admin.AddLDAPProviderRequest{
Name: name,
Servers: []string{"servers"},
StartTls: true,
BaseDn: "baseDN",
BindDn: "bindND",
BindPassword: "bindPassword",
UserBase: "userBase",
UserObjectClasses: []string{"userOhjectClasses"},
UserFilters: []string{"userFilters"},
Timeout: durationpb.New(time.Minute),
Attributes: &idp_grpc.LDAPAttributes{
IdAttribute: "idAttribute",
FirstNameAttribute: "firstNameAttribute",
LastNameAttribute: "lastNameAttribute",
DisplayNameAttribute: "displayNameAttribute",
NickNameAttribute: "nickNameAttribute",
PreferredUsernameAttribute: "preferredUsernameAttribute",
EmailAttribute: "emailAttribute",
EmailVerifiedAttribute: "emailVerifiedAttribute",
PhoneAttribute: "phoneAttribute",
PhoneVerifiedAttribute: "phoneVerifiedAttribute",
PreferredLanguageAttribute: "preferredLanguageAttribute",
AvatarUrlAttribute: "avatarUrlAttribute",
ProfileAttribute: "profileAttribute",
},
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
})
addLdap, err := AdminClient.AddLDAPProvider(IAMCTX, defaultInstanceLDAPRequest(name))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -2328,25 +2192,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
// add saml
before := time.Now()
addSAML, err := AdminClient.AddSAMLProvider(IAMCTX, &admin.AddSAMLProviderRequest{
Name: name,
Metadata: &admin.AddSAMLProviderRequest_MetadataXml{
MetadataXml: validSAMLMetadata1,
},
Binding: idp.SAMLBinding_SAML_BINDING_POST,
WithSignedRequest: false,
TransientMappingAttributeName: &name,
FederatedLogoutEnabled: &federatedLogoutEnabled,
NameIdFormat: idp.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT.Enum(),
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
SignatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA1,
})
addSAML, err := AdminClient.AddSAMLProvider(IAMCTX, defaultInstanceSAMLRequest(name, &federatedLogoutEnabled))
after := time.Now()
require.NoError(t, err)
@@ -2389,25 +2235,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
federatedLogoutEnabled := false
// add saml
addSAML, err := AdminClient.AddSAMLProvider(IAMCTX, &admin.AddSAMLProviderRequest{
Name: name,
Metadata: &admin.AddSAMLProviderRequest_MetadataXml{
MetadataXml: validSAMLMetadata1,
},
Binding: idp.SAMLBinding_SAML_BINDING_POST,
WithSignedRequest: false,
TransientMappingAttributeName: &name,
FederatedLogoutEnabled: &federatedLogoutEnabled,
NameIdFormat: idp.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT.Enum(),
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
SignatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA1,
})
addSAML, err := AdminClient.AddSAMLProvider(IAMCTX, defaultInstanceSAMLRequest(name, &federatedLogoutEnabled))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -2484,17 +2312,7 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
name := gofakeit.Name()
// add idp
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: true,
})
addOIDC, err := AdminClient.AddOIDCIDP(IAMCTX, defaultOIDCIDReq(name, true))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -2528,3 +2346,76 @@ func TestServer_TestIDProviderInstanceReduces(t *testing.T) {
}, retryDuration, tick)
})
}
func defaultInstanceSAMLRequest(name string, federatedLogoutEnable *bool) *admin.AddSAMLProviderRequest {
return &admin.AddSAMLProviderRequest{
Name: name,
Metadata: &admin.AddSAMLProviderRequest_MetadataXml{
MetadataXml: validSAMLMetadata1,
},
Binding: idp.SAMLBinding_SAML_BINDING_POST,
WithSignedRequest: false,
TransientMappingAttributeName: &name,
FederatedLogoutEnabled: federatedLogoutEnable,
NameIdFormat: idp.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT.Enum(),
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
SignatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA1,
}
}
func defaultOIDCIDReq(name string, autoRegister bool) *admin.AddOIDCIDPRequest {
return &admin.AddOIDCIDPRequest{
Name: name,
StylingType: idp_grpc.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "clientID",
ClientSecret: "clientSecret",
Issuer: "issuer",
Scopes: []string{"scope"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
AutoRegister: autoRegister,
}
}
func defaultInstanceLDAPRequest(name string) *admin.AddLDAPProviderRequest {
return &admin.AddLDAPProviderRequest{
Name: name,
Servers: []string{"servers"},
StartTls: true,
BaseDn: "baseDN",
BindDn: "bindND",
BindPassword: "bindPassword",
UserBase: "userBase",
UserObjectClasses: []string{"userOhjectClasses"},
UserFilters: []string{"userFilters"},
Timeout: durationpb.New(time.Minute),
Attributes: &idp_grpc.LDAPAttributes{
IdAttribute: "idAttribute",
FirstNameAttribute: "firstNameAttribute",
LastNameAttribute: "lastNameAttribute",
DisplayNameAttribute: "displayNameAttribute",
NickNameAttribute: "nickNameAttribute",
PreferredUsernameAttribute: "preferredUsernameAttribute",
EmailAttribute: "emailAttribute",
EmailVerifiedAttribute: "emailVerifiedAttribute",
PhoneAttribute: "phoneAttribute",
PhoneVerifiedAttribute: "phoneVerifiedAttribute",
PreferredLanguageAttribute: "preferredLanguageAttribute",
AvatarUrlAttribute: "avatarUrlAttribute",
ProfileAttribute: "profileAttribute",
},
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
}
}
@@ -1852,40 +1852,7 @@ func TestServer_TestIDProviderOrgReduces(t *testing.T) {
// add ldap
before := time.Now()
addLdap, err := MgmtClient.AddLDAPProvider(IAMCTX, &management.AddLDAPProviderRequest{
Name: name,
Servers: []string{"servers"},
StartTls: true,
BaseDn: "baseDN",
BindDn: "bindND",
BindPassword: "bindPassword",
UserBase: "userBase",
UserObjectClasses: []string{"userOhjectClasses"},
UserFilters: []string{"userFilters"},
Timeout: durationpb.New(time.Minute),
Attributes: &idp_grpc.LDAPAttributes{
IdAttribute: "idAttribute",
FirstNameAttribute: "firstNameAttribute",
LastNameAttribute: "lastNameAttribute",
DisplayNameAttribute: "displayNameAttribute",
NickNameAttribute: "nickNameAttribute",
PreferredUsernameAttribute: "preferredUsernameAttribute",
EmailAttribute: "emailAttribute",
EmailVerifiedAttribute: "emailVerifiedAttribute",
PhoneAttribute: "phoneAttribute",
PhoneVerifiedAttribute: "phoneVerifiedAttribute",
PreferredLanguageAttribute: "preferredLanguageAttribute",
AvatarUrlAttribute: "avatarUrlAttribute",
ProfileAttribute: "profileAttribute",
},
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
})
addLdap, err := MgmtClient.AddLDAPProvider(IAMCTX, defaultOrganizationLDAPRequest(name))
after := time.Now()
require.NoError(t, err)
@@ -1940,40 +1907,7 @@ func TestServer_TestIDProviderOrgReduces(t *testing.T) {
name := gofakeit.Name()
// add ldap
addLdap, err := MgmtClient.AddLDAPProvider(IAMCTX, &management.AddLDAPProviderRequest{
Name: name,
Servers: []string{"servers"},
StartTls: true,
BaseDn: "baseDN",
BindDn: "bindND",
BindPassword: "bindPassword",
UserBase: "userBase",
UserObjectClasses: []string{"userOhjectClasses"},
UserFilters: []string{"userFilters"},
Timeout: durationpb.New(time.Minute),
Attributes: &idp_grpc.LDAPAttributes{
IdAttribute: "idAttribute",
FirstNameAttribute: "firstNameAttribute",
LastNameAttribute: "lastNameAttribute",
DisplayNameAttribute: "displayNameAttribute",
NickNameAttribute: "nickNameAttribute",
PreferredUsernameAttribute: "preferredUsernameAttribute",
EmailAttribute: "emailAttribute",
EmailVerifiedAttribute: "emailVerifiedAttribute",
PhoneAttribute: "phoneAttribute",
PhoneVerifiedAttribute: "phoneVerifiedAttribute",
PreferredLanguageAttribute: "preferredLanguageAttribute",
AvatarUrlAttribute: "avatarUrlAttribute",
ProfileAttribute: "profileAttribute",
},
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
})
addLdap, err := MgmtClient.AddLDAPProvider(IAMCTX, defaultOrganizationLDAPRequest(name))
require.NoError(t, err)
idpRepo := repository.IDProviderRepository()
@@ -2412,3 +2346,40 @@ func TestServer_TestIDProviderOrgReduces(t *testing.T) {
}, retryDuration, tick)
})
}
func defaultOrganizationLDAPRequest(providerName string) *management.AddLDAPProviderRequest {
return &management.AddLDAPProviderRequest{
Name: providerName,
Servers: []string{"servers"},
StartTls: true,
BaseDn: "baseDN",
BindDn: "bindND",
BindPassword: "bindPassword",
UserBase: "userBase",
UserObjectClasses: []string{"userOhjectClasses"},
UserFilters: []string{"userFilters"},
Timeout: durationpb.New(time.Minute),
Attributes: &idp_grpc.LDAPAttributes{
IdAttribute: "idAttribute",
FirstNameAttribute: "firstNameAttribute",
LastNameAttribute: "lastNameAttribute",
DisplayNameAttribute: "displayNameAttribute",
NickNameAttribute: "nickNameAttribute",
PreferredUsernameAttribute: "preferredUsernameAttribute",
EmailAttribute: "emailAttribute",
EmailVerifiedAttribute: "emailVerifiedAttribute",
PhoneAttribute: "phoneAttribute",
PhoneVerifiedAttribute: "phoneVerifiedAttribute",
PreferredLanguageAttribute: "preferredLanguageAttribute",
AvatarUrlAttribute: "avatarUrlAttribute",
ProfileAttribute: "profileAttribute",
},
ProviderOptions: &idp_grpc.Options{
IsLinkingAllowed: false,
IsCreationAllowed: false,
IsAutoCreation: false,
IsAutoUpdate: false,
AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL,
},
}
}
@@ -0,0 +1,396 @@
//go:build integration
package events_test
// TODO(adlerhurst): Uncomment once users table is created and FK can be enforced
// import (
// "net"
// "net/url"
// "testing"
// "time"
// "github.com/brianvoe/gofakeit/v6"
// "github.com/muhlemmer/gu"
// "github.com/stretchr/testify/assert"
// "github.com/stretchr/testify/require"
// durationpb "google.golang.org/protobuf/types/known/durationpb"
// "github.com/zitadel/zitadel/backend/v3/domain"
// "github.com/zitadel/zitadel/backend/v3/storage/database"
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
// "github.com/zitadel/zitadel/internal/idp/providers/ldap"
// "github.com/zitadel/zitadel/internal/integration"
// "github.com/zitadel/zitadel/internal/integration/sink"
// "github.com/zitadel/zitadel/pkg/grpc/admin"
// "github.com/zitadel/zitadel/pkg/grpc/session/v2"
// "github.com/zitadel/zitadel/pkg/grpc/user/v2"
// )
// func TestServer_IDPIntentReduces(t *testing.T) {
// retryDuration, tick := integration.WaitForAndTickWithMaxDuration(IAMCTX, time.Minute)
// idpIntentRepo := repository.IDPIntentRepository()
// instanceID := Instance.ID()
// t.Run("start LDAP ID provider intent should create intent and succeed", func(t *testing.T) {
// ldapProvider, err := AdminClient.AddLDAPProvider(IAMCTX, defaultInstanceLDAPRequest(gofakeit.Name()))
// require.NoError(t, err)
// req := &user.StartIdentityProviderIntentRequest{
// IdpId: ldapProvider.GetId(),
// Content: &user.StartIdentityProviderIntentRequest_Ldap{
// Ldap: &user.LDAPCredentials{
// Username: "some-user",
// Password: "some-pass",
// },
// },
// }
// res, err := UserClient.StartIdentityProviderIntent(IAMCTX, req)
// require.NoError(t, err)
// idpIntent, ok := res.GetNextStep().(*user.StartIdentityProviderIntentResponse_IdpIntent)
// require.True(t, ok)
// t.Cleanup(func() {
// idpIntentRepo.Delete(IAMCTX, pool, idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntent.IdpIntent.GetIdpIntentId()))
// AdminClient.RemoveIDP(IAMCTX, &admin.RemoveIDPRequest{IdpId: ldapProvider.GetId()})
// })
// require.EventuallyWithT(t, func(collect *assert.CollectT) {
// retrievedRelationalIntent, err := idpIntentRepo.Get(
// IAMCTX,
// pool,
// database.WithCondition(database.And(
// idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntent.IdpIntent.GetIdpIntentId()),
// idpIntentRepo.StateCondition(domain.IDPIntentStateSucceeded),
// )),
// )
// require.NoError(collect, err)
// assert.Equal(collect, domain.IDPIntentStateSucceeded, retrievedRelationalIntent.State)
// assert.Empty(collect, retrievedRelationalIntent.SuccessURL)
// assert.Empty(collect, retrievedRelationalIntent.FailureURL)
// assert.Equal(collect, ldapProvider.GetId(), retrievedRelationalIntent.IDPID)
// assert.Nil(collect, retrievedRelationalIntent.IDPArguments)
// assert.NotEmpty(collect, retrievedRelationalIntent.IDPUser)
// assert.NotEmpty(collect, retrievedRelationalIntent.IDPUserID)
// assert.NotEmpty(collect, retrievedRelationalIntent.EntryAttributes)
// assert.True(collect, retrievedRelationalIntent.ExpiresAt.IsZero())
// }, retryDuration, tick)
// })
// t.Run("oidc intent flow started with URLs should create intent in started state", func(t *testing.T) {
// idpCreateReq := defaultOIDCIDReq(gofakeit.Name(), true)
// idpCreateReq.Issuer = "http://localhost:8082"
// oidcIDP, err := AdminClient.AddOIDCIDP(IAMCTX, idpCreateReq)
// require.NoError(t, err)
// req := &user.StartIdentityProviderIntentRequest{
// IdpId: oidcIDP.GetIdpId(),
// Content: &user.StartIdentityProviderIntentRequest_Urls{
// Urls: &user.RedirectURLs{
// SuccessUrl: "https://localhost:8081/success",
// FailureUrl: "https://localhost:8081/fail",
// },
// },
// }
// res, err := UserClient.StartIdentityProviderIntent(IAMCTX, req)
// require.NoError(t, err)
// authURL, ok := res.GetNextStep().(*user.StartIdentityProviderIntentResponse_AuthUrl)
// require.True(t, ok)
// parsedAuthURL, err := url.Parse(authURL.AuthUrl)
// require.NoError(t, err)
// idpIntentID := parsedAuthURL.Query().Get("state")
// t.Cleanup(func() {
// idpIntentRepo.Delete(IAMCTX, pool, idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID))
// AdminClient.RemoveIDP(IAMCTX, &admin.RemoveIDPRequest{IdpId: oidcIDP.GetIdpId()})
// })
// require.EventuallyWithT(t, func(collect *assert.CollectT) {
// retrievedRelationalIntent, err := idpIntentRepo.Get(
// IAMCTX,
// pool,
// database.WithCondition(idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID)),
// )
// require.NoError(collect, err)
// assert.Equal(collect, domain.IDPIntentStateStarted, retrievedRelationalIntent.State)
// assert.Equal(collect, "https://localhost:8081/success", retrievedRelationalIntent.SuccessURL.String())
// assert.Equal(collect, "https://localhost:8081/fail", retrievedRelationalIntent.FailureURL.String())
// assert.Empty(collect, retrievedRelationalIntent.IDPArguments)
// }, retryDuration, tick)
// })
// t.Run("when successful oidc intent should update intent state to succeeded", func(t *testing.T) {
// beforeCreate := time.Now()
// idpCreateReq := defaultOIDCIDReq(gofakeit.Name(), true)
// idpCreateReq.Issuer = "http://localhost:8082"
// oidcIDP, err := AdminClient.AddOIDCIDP(IAMCTX, idpCreateReq)
// require.NoError(t, err)
// expiryDate := time.Now().Add(time.Minute * 30)
// idpIntentID, _, _, _, err := sink.SuccessfulOIDCIntent(instanceID, oidcIDP.GetIdpId(), "some-idp-user", "some-user", expiryDate)
// require.NoError(t, err)
// afterSuccess := time.Now()
// t.Cleanup(func() {
// idpIntentRepo.Delete(IAMCTX, pool, idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID))
// AdminClient.RemoveIDP(IAMCTX, &admin.RemoveIDPRequest{IdpId: oidcIDP.GetIdpId()})
// })
// require.EventuallyWithT(t, func(collect *assert.CollectT) {
// retrievedRelationalIntent, err := idpIntentRepo.Get(
// IAMCTX,
// pool,
// database.WithCondition(idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID)),
// )
// require.NoError(collect, err)
// assert.Equal(collect, domain.IDPIntentStateSucceeded, retrievedRelationalIntent.State)
// assert.NotEmpty(collect, retrievedRelationalIntent.IDPUser)
// assert.Equal(collect, "some-idp-user", retrievedRelationalIntent.IDPUserID)
// assert.Equal(collect, "username", retrievedRelationalIntent.IDPUsername)
// assert.Equal(collect, "some-user", retrievedRelationalIntent.UserID)
// assert.NotEmpty(collect, retrievedRelationalIntent.IDPAccessToken)
// assert.Equal(collect, "idToken", retrievedRelationalIntent.IDPIDToken)
// require.NotNil(collect, retrievedRelationalIntent.SucceededAt)
// assert.WithinRange(collect, *retrievedRelationalIntent.SucceededAt, beforeCreate, afterSuccess)
// require.NotNil(collect, retrievedRelationalIntent.ExpiresAt)
// assert.WithinRange(collect, *retrievedRelationalIntent.ExpiresAt, expiryDate.Add(time.Millisecond*-5), expiryDate.Add(time.Second*1))
// }, retryDuration, tick)
// })
// t.Run("when successful SAML intent should update intent state to succeeded", func(t *testing.T) {
// beforeCreate := time.Now()
// samlIDP, err := AdminClient.AddSAMLProvider(IAMCTX, defaultInstanceSAMLRequest(gofakeit.Name(), gu.Ptr(true)))
// require.NoError(t, err)
// expiryDate := time.Now().Add(time.Minute * 30)
// idpIntentID, _, _, _, err := sink.SuccessfulSAMLIntent(instanceID, samlIDP.GetId(), "some-idp-user", "some-user", "", expiryDate)
// require.NoError(t, err)
// afterSuccess := time.Now()
// t.Cleanup(func() {
// idpIntentRepo.Delete(IAMCTX, pool, idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID))
// AdminClient.RemoveIDP(IAMCTX, &admin.RemoveIDPRequest{IdpId: samlIDP.GetId()})
// })
// require.EventuallyWithT(t, func(collect *assert.CollectT) {
// retrievedRelationalIntent, err := idpIntentRepo.Get(
// IAMCTX,
// pool,
// database.WithCondition(idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID)),
// )
// require.NoError(collect, err)
// assert.Equal(collect, domain.IDPIntentStateSucceeded, retrievedRelationalIntent.State)
// assert.NotEmpty(collect, retrievedRelationalIntent.IDPUser)
// assert.Equal(collect, "some-idp-user", retrievedRelationalIntent.IDPUserID)
// assert.Empty(collect, retrievedRelationalIntent.IDPUsername)
// assert.Equal(collect, "some-user", retrievedRelationalIntent.UserID)
// assert.NotEmpty(collect, retrievedRelationalIntent.Assertion)
// require.NotNil(collect, retrievedRelationalIntent.SucceededAt)
// assert.WithinRange(collect, *retrievedRelationalIntent.SucceededAt, beforeCreate, afterSuccess)
// require.NotNil(collect, retrievedRelationalIntent.ExpiresAt)
// assert.WithinRange(collect, *retrievedRelationalIntent.ExpiresAt, expiryDate.Add(time.Millisecond*-5), expiryDate.Add(time.Second*1))
// }, retryDuration, tick)
// })
// t.Run("when emitting SAMLRequest event should update intent with request ID", func(t *testing.T) {
// samlIDP, err := AdminClient.AddSAMLProvider(IAMCTX, defaultInstanceSAMLRequest(gofakeit.Name(), gu.Ptr(true)))
// require.NoError(t, err)
// expiry := time.Now().Add(time.Hour * 1)
// req := &user.StartIdentityProviderIntentRequest{
// IdpId: samlIDP.GetId(),
// Content: &user.StartIdentityProviderIntentRequest_Urls{
// Urls: &user.RedirectURLs{
// SuccessUrl: "https://localhost:8081/success",
// FailureUrl: "https://localhost:8081/fail",
// },
// },
// }
// res, err := UserClient.StartIdentityProviderIntent(IAMCTX, req)
// require.NoError(t, err)
// formData, ok := res.GetNextStep().(*user.StartIdentityProviderIntentResponse_FormData)
// require.True(t, ok)
// idpIntentID, ok := formData.FormData.GetFields()["RelayState"]
// require.True(t, ok)
// t.Cleanup(func() {
// idpIntentRepo.Delete(IAMCTX, pool, idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID))
// AdminClient.RemoveIDP(IAMCTX, &admin.RemoveIDPRequest{IdpId: samlIDP.GetId()})
// })
// idpIntentID, token, _, _, err := sink.SuccessfulSAMLIntent(instanceID, samlIDP.GetId(), "idp-user-id", "some-user", idpIntentID, expiry)
// require.NoError(t, err)
// _, err = UserClient.RetrieveIdentityProviderIntent(IAMCTX, &user.RetrieveIdentityProviderIntentRequest{
// IdpIntentId: idpIntentID,
// IdpIntentToken: token,
// })
// require.NoError(t, err)
// require.EventuallyWithT(t, func(collect *assert.CollectT) {
// retrievedRelationalIntent, err := idpIntentRepo.Get(
// IAMCTX,
// pool,
// database.WithCondition(database.And(
// idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID),
// )),
// )
// require.NoError(collect, err)
// assert.NotEmpty(collect, retrievedRelationalIntent.RequestID)
// }, retryDuration, tick)
// })
// t.Run("when LDAP intent flow fails should update intent state and reason", func(t *testing.T) {
// beforeCreate := time.Now()
// ldapProvider, err := AdminClient.AddLDAPProvider(IAMCTX, defaultInstanceLDAPRequest(gofakeit.Name()))
// require.NoError(t, err)
// req := &user.StartIdentityProviderIntentRequest{
// IdpId: ldapProvider.GetId(),
// Content: &user.StartIdentityProviderIntentRequest_Ldap{
// Ldap: &user.LDAPCredentials{
// Username: ldap.FailingUser,
// Password: ldap.FailingPsw,
// },
// },
// }
// _, err = UserClient.StartIdentityProviderIntent(IAMCTX, req)
// require.Error(t, err)
// afterFail := time.Now()
// var idpIntentID string
// t.Cleanup(func() {
// idpIntentRepo.Delete(IAMCTX, pool, idpIntentRepo.PrimaryKeyCondition(instanceID, idpIntentID))
// AdminClient.RemoveIDP(IAMCTX, &admin.RemoveIDPRequest{IdpId: ldapProvider.GetId()})
// })
// require.EventuallyWithT(t, func(collect *assert.CollectT) {
// retrievedRelationalIntent, err := idpIntentRepo.Get(
// IAMCTX,
// pool,
// database.WithCondition(database.And(
// idpIntentRepo.InstanceIDCondition(instanceID),
// idpIntentRepo.StateCondition(domain.IDPIntentStateFailed),
// )),
// database.WithOrderByDescending(idpIntentRepo.CreatedAtColumn()),
// )
// require.NoError(collect, err)
// assert.Equal(collect, domain.IDPIntentStateFailed, retrievedRelationalIntent.State)
// assert.NotEmpty(collect, retrievedRelationalIntent.FailReason)
// require.NotNil(collect, retrievedRelationalIntent.FailedAt)
// assert.WithinRange(collect, *retrievedRelationalIntent.FailedAt, beforeCreate, afterFail)
// idpIntentID = retrievedRelationalIntent.ID
// }, retryDuration, tick)
// })
// t.Run("when session update checks intent should delete intent", func(t *testing.T) {
// // ==========
// // GIVEN
// // ==========
// // Create a user
// email := integration.Email()
// testUser := Instance.CreateUserTypeHuman(IAMCTX, email)
// _, err := UserClient.VerifyEmail(IAMCTX, &user.VerifyEmailRequest{
// UserId: testUser.GetId(),
// VerificationCode: testUser.GetEmailCode(),
// })
// require.NoError(t, err)
// _, err = UserClient.SetPassword(CTX, &user.SetPasswordRequest{
// UserId: testUser.GetId(),
// NewPassword: &user.Password{
// Password: integration.UserPassword,
// },
// Verification: nil,
// })
// require.NoError(t, err)
// // Create an ID Provider
// idpID := Instance.AddGenericOAuthProvider(IAMCTX, integration.IDPName()).GetId()
// // Link the user with the provider
// idpUserID := integration.ID()
// _, err = UserClient.AddIDPLink(IAMCTX, &user.AddIDPLinkRequest{
// UserId: testUser.GetId(),
// IdpLink: &user.IDPLink{
// IdpId: idpID,
// UserId: idpUserID,
// UserName: integration.Username(),
// },
// })
// require.NoError(t, err)
// // Create an IDP intent
// _, err = url.Parse(Instance.CreateIntent(CTX, idpID).GetAuthUrl())
// require.NoError(t, err)
// expiry := time.Now().Add(1 * time.Hour)
// // Fake a successful intent flow
// intentID, intentToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, testUser.GetId(), expiry)
// require.NoError(t, err)
// // Create a session for the user
// createdSessionID := createSessionForUser(t)
// require.NoError(t, err)
// // ==========
// // TEST
// // ==========
// // Update the session with user, password and IPDIntent checks
// _, err = SessionClient.SetSession(IAMCTX, &session.SetSessionRequest{
// SessionId: createdSessionID,
// Checks: &session.Checks{
// User: &session.CheckUser{
// Search: &session.CheckUser_LoginName{
// LoginName: email,
// },
// },
// Password: &session.CheckPassword{
// Password: integration.UserPassword,
// },
// IdpIntent: &session.CheckIDPIntent{
// IdpIntentId: intentID,
// IdpIntentToken: intentToken,
// },
// },
// })
// require.NoError(t, err)
// // ==========
// // VERIFY
// // ==========
// t.Cleanup(func() {
// idpIntentRepo.Delete(IAMCTX, pool, idpIntentRepo.PrimaryKeyCondition(instanceID, intentID))
// AdminClient.RemoveIDP(IAMCTX, &admin.RemoveIDPRequest{IdpId: idpID})
// SessionClient.DeleteSession(IAMCTX, &session.DeleteSessionRequest{SessionId: createdSessionID})
// })
// require.EventuallyWithT(t, func(collect *assert.CollectT) {
// _, err := idpIntentRepo.Get(
// IAMCTX,
// pool,
// database.WithCondition(idpIntentRepo.PrimaryKeyCondition(instanceID, intentID)),
// )
// assert.Error(collect, err)
// }, retryDuration, tick)
// })
// }
// func createSessionForUser(t *testing.T) (sessionID string) {
// t.Helper()
// lifetime := time.Hour
// createdSession, err := SessionClient.CreateSession(CTX, &session.CreateSessionRequest{
// Checks: nil,
// Metadata: nil,
// Challenges: nil,
// UserAgent: &session.UserAgent{
// FingerprintId: gu.Ptr(integration.ID()),
// Ip: gu.Ptr(net.IPv4(127, 0, 0, 1).String()),
// Description: gu.Ptr("description"),
// Header: map[string]*session.UserAgent_HeaderValues{
// "User-Agent": {Values: []string{"ZITADEL-Integration-Test"}},
// },
// },
// Lifetime: durationpb.New(lifetime),
// })
// require.NoError(t, err)
// return createdSession.GetSessionId()
// }
@@ -28,19 +28,7 @@ func TestCreateIDProvider(t *testing.T) {
}()
// create instance
instanceId := gofakeit.Name()
instance := domain.Instance{
ID: instanceId,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "managementConsoleCLient",
ConsoleAppID: "managementConsoleApp",
DefaultLanguage: "defaultLanguage",
}
instanceRepo := repository.InstanceRepository()
err = instanceRepo.Create(t.Context(), tx, &instance)
require.NoError(t, err)
instanceId := createInstance(t, tx)
// create org
orgId := gofakeit.Name()
@@ -61,7 +49,6 @@ func TestCreateIDProvider(t *testing.T) {
err error
}
// TESTS
tests := []test{
{
name: "happy path",
@@ -83,11 +70,10 @@ func TestCreateIDProvider(t *testing.T) {
{
name: "create idp without name",
idp: domain.IdentityProvider{
InstanceID: instanceId,
OrgID: &orgId,
ID: gofakeit.Name(),
State: domain.IDPStateActive,
// Name: gofakeit.Name(),
InstanceID: instanceId,
OrgID: &orgId,
ID: gofakeit.Name(),
State: domain.IDPStateActive,
Type: gu.Ptr(domain.IDPTypeOIDC),
AllowCreation: true,
AllowAutoCreation: true,
@@ -231,9 +217,8 @@ func TestCreateIDProvider(t *testing.T) {
{
name: "adding idp with no id",
idp: domain.IdentityProvider{
InstanceID: instanceId,
OrgID: &orgId,
// ID: gofakeit.Name(),
InstanceID: instanceId,
OrgID: &orgId,
State: domain.IDPStateActive,
Name: gofakeit.Name(),
Type: gu.Ptr(domain.IDPTypeOIDC),
@@ -249,11 +234,10 @@ func TestCreateIDProvider(t *testing.T) {
{
name: "adding idp with no name",
idp: domain.IdentityProvider{
InstanceID: instanceId,
OrgID: &orgId,
ID: gofakeit.Name(),
State: domain.IDPStateActive,
// Name: gofakeit.Name(),
InstanceID: instanceId,
OrgID: &orgId,
ID: gofakeit.Name(),
State: domain.IDPStateActive,
Type: gu.Ptr(domain.IDPTypeOIDC),
AllowCreation: true,
AllowAutoCreation: true,
@@ -267,7 +251,6 @@ func TestCreateIDProvider(t *testing.T) {
{
name: "adding idp with no instance id",
idp: domain.IdentityProvider{
// InstanceID: instanceId,
OrgID: &orgId,
State: domain.IDPStateActive,
Name: gofakeit.Name(),
@@ -384,31 +367,10 @@ func TestUpdateIDProvider(t *testing.T) {
}()
// create instance
instanceId := gofakeit.Name()
instance := domain.Instance{
ID: instanceId,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "managementConsoleCLient",
ConsoleAppID: "managementConsoleApp",
DefaultLanguage: "defaultLanguage",
}
instanceRepo := repository.InstanceRepository()
err = instanceRepo.Create(t.Context(), tx, &instance)
require.NoError(t, err)
instanceId := createInstance(t, tx)
// create org
orgId := gofakeit.Name()
org := domain.Organization{
ID: orgId,
Name: gofakeit.Name(),
InstanceID: instanceId,
State: domain.OrgStateActive,
}
organizationRepo := repository.OrganizationRepository()
err = organizationRepo.Create(t.Context(), tx, &org)
require.NoError(t, err)
orgId := createOrganization(t, tx, instanceId)
idpRepo := repository.IDProviderRepository()
@@ -669,44 +631,12 @@ func TestGetIDProvider(t *testing.T) {
}
}()
// create instance
instanceId := gofakeit.Name()
instance := domain.Instance{
ID: instanceId,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "managementConsoleCLient",
ConsoleAppID: "managementConsoleApp",
DefaultLanguage: "defaultLanguage",
}
instanceRepo := repository.InstanceRepository()
err = instanceRepo.Create(t.Context(), tx, &instance)
require.NoError(t, err)
instanceId := createInstance(t, tx)
orgId := createOrganization(t, tx, instanceId)
// create org
orgId := gofakeit.Name()
org := domain.Organization{
ID: orgId,
Name: gofakeit.Name(),
InstanceID: instanceId,
State: domain.OrgStateActive,
}
organizationRepo := repository.OrganizationRepository()
err = organizationRepo.Create(t.Context(), tx, &org)
require.NoError(t, err)
// create organization
// this org is created as an additional org which should NOT
// be returned in the results of the tests
preexistingOrg := domain.Organization{
ID: gofakeit.Name(),
Name: gofakeit.Name(),
InstanceID: instanceId,
State: domain.OrgStateActive,
}
err = organizationRepo.Create(t.Context(), tx, &preexistingOrg)
require.NoError(t, err)
createOrganization(t, tx, instanceId)
idpRepo := repository.IDProviderRepository()
type test struct {
@@ -955,31 +885,10 @@ func TestListIDProvider(t *testing.T) {
}()
// create instance
instanceId := gofakeit.Name()
instance := domain.Instance{
ID: instanceId,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "managementConsoleCLient",
ConsoleAppID: "managementConsoleApp",
DefaultLanguage: "defaultLanguage",
}
instanceRepo := repository.InstanceRepository()
err = instanceRepo.Create(t.Context(), tx, &instance)
require.NoError(t, err)
instanceId := createInstance(t, tx)
// create org
orgId := gofakeit.Name()
org := domain.Organization{
ID: orgId,
Name: gofakeit.Name(),
InstanceID: instanceId,
State: domain.OrgStateActive,
}
organizationRepo := repository.OrganizationRepository()
err = organizationRepo.Create(t.Context(), tx, &org)
require.NoError(t, err)
orgId := createOrganization(t, tx, instanceId)
idpRepo := repository.IDProviderRepository()
@@ -994,30 +903,10 @@ func TestListIDProvider(t *testing.T) {
name: "multiple idps filter on instance",
testFunc: func(t *testing.T) []*domain.IdentityProvider {
// create instance
newInstanceId := gofakeit.Name()
instance := domain.Instance{
ID: newInstanceId,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "managementConsoleCLient",
ConsoleAppID: "managementConsoleApp",
DefaultLanguage: "defaultLanguage",
}
err = instanceRepo.Create(t.Context(), tx, &instance)
require.NoError(t, err)
newInstanceId := createInstance(t, tx)
// create org
newOrgId := gofakeit.Name()
org := domain.Organization{
ID: newOrgId,
Name: gofakeit.Name(),
InstanceID: newInstanceId,
State: domain.OrgStateActive,
}
organizationRepo := repository.OrganizationRepository()
err = organizationRepo.Create(t.Context(), tx, &org)
require.NoError(t, err)
newOrgId := createOrganization(t, tx, newInstanceId)
// create idp
// this idp is created as an additional idp which should NOT
@@ -1785,31 +1674,10 @@ func TestDeleteIDProvider(t *testing.T) {
}()
// create instance
instanceId := gofakeit.Name()
instance := domain.Instance{
ID: instanceId,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "managementConsoleCLient",
ConsoleAppID: "managementConsoleApp",
DefaultLanguage: "defaultLanguage",
}
instanceRepo := repository.InstanceRepository()
err = instanceRepo.Create(t.Context(), tx, &instance)
require.NoError(t, err)
instanceId := createInstance(t, tx)
// create org
orgId := gofakeit.Name()
org := domain.Organization{
ID: orgId,
Name: gofakeit.Name(),
InstanceID: instanceId,
State: domain.OrgStateActive,
}
organizationRepo := repository.OrganizationRepository()
err = organizationRepo.Create(t.Context(), tx, &org)
require.NoError(t, err)
orgId := createOrganization(t, tx, instanceId)
idpRepo := repository.IDProviderRepository()
@@ -0,0 +1,442 @@
package repository
import (
"context"
"fmt"
"net/url"
"time"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type idpIntentRepository struct{}
func (i idpIntentRepository) qualifiedTableName() string {
return "zitadel.identity_provider_intents"
}
func (i idpIntentRepository) unqualifiedTableName() string {
return "identity_provider_intents"
}
func IDPIntentRepository() domain.IDPIntentRepository {
return new(idpIntentRepository)
}
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
const queryIDPIntentStmt = `
SELECT
identity_provider_intents.instance_id
, identity_provider_intents.id
, identity_provider_intents.state
, identity_provider_intents.success_url
, identity_provider_intents.failure_url
, identity_provider_intents.created_at
, identity_provider_intents.updated_at
, identity_provider_intents.idp_id
, identity_provider_intents.idp_arguments
, identity_provider_intents.idp_user
, identity_provider_intents.idp_user_id
, identity_provider_intents.idp_username
, identity_provider_intents.user_id
, identity_provider_intents.idp_access_token
, identity_provider_intents.idp_id_token
, identity_provider_intents.idp_entry_attributes
, identity_provider_intents.request_id
, identity_provider_intents.assertion
, identity_provider_intents.succeeded_at
, identity_provider_intents.failed_at
, identity_provider_intents.fail_reason
, identity_provider_intents.expires_at
FROM
zitadel.identity_provider_intents
`
// Create implements [domain.IDPIntentRepository].
func (i idpIntentRepository) Create(ctx context.Context, client database.QueryExecutor, intent *domain.IDPIntent) error {
var createdAt any = database.DefaultInstruction
if !intent.CreatedAt.IsZero() {
createdAt = intent.CreatedAt
}
var updatedAt = createdAt
if !intent.UpdatedAt.IsZero() {
updatedAt = intent.UpdatedAt
}
builder := new(database.StatementBuilder)
builder.WriteString(`INSERT INTO ` + i.qualifiedTableName() + ` (instance_id, id, success_url, failure_url, idp_id, idp_arguments, created_at, updated_at) VALUES ( `)
builder.WriteArgs(intent.InstanceID, intent.ID, intent.SuccessURL.String(), intent.FailureURL.String(), intent.IDPID, intent.IDPArguments, createdAt, updatedAt)
builder.WriteString(` ) RETURNING created_at, updated_at`)
return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&intent.CreatedAt, &intent.UpdatedAt)
}
// Delete implements [domain.IDPIntentRepository].
func (i idpIntentRepository) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
return deleteOne(ctx, client, i, condition)
}
// Get implements [domain.IDPIntentRepository].
func (i idpIntentRepository) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.IDPIntent, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
if options.Condition == nil || !options.Condition.IsRestrictingColumn(i.InstanceIDColumn()) {
return nil, database.NewMissingConditionError(i.InstanceIDColumn())
}
var builder database.StatementBuilder
builder.WriteString(queryIDPIntentStmt)
options.Write(&builder)
fmt.Println(builder.String())
return scanIDPIntent(ctx, client, &builder)
}
type rawIDPIntent struct {
*domain.IDPIntent
SuccessURL string `json:"success_url,omitempty" db:"success_url"`
FailureURL string `json:"failure_url,omitempty" db:"failure_url"`
IDPUserID *string `json:"idp_user_id,omitempty" db:"idp_user_id"`
IDPUsername *string `json:"idp_username,omitempty" db:"idp_username"`
UserID *string `json:"user_id,omitempty" db:"user_id"`
IDPIDToken *string `json:"idp_id_token,omitempty" db:"idp_id_token"`
RequestID *string `json:"request_id,omitempty" db:"request_id"`
FailReason *string `json:"fail_reason,omitempty" db:"fail_reason"`
}
func scanIDPIntent(ctx context.Context, querier database.QueryExecutor, builder *database.StatementBuilder) (*domain.IDPIntent, error) {
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
if err != nil {
return nil, err
}
raw := new(rawIDPIntent)
if err := rows.(database.CollectableRows).CollectExactlyOneRow(raw); err != nil {
return nil, err
}
return rawIDPIntentToDomain(raw)
}
func rawIDPIntentToDomain(raw *rawIDPIntent) (*domain.IDPIntent, error) {
successURL, _ := url.Parse(raw.SuccessURL)
failureURL, _ := url.Parse(raw.FailureURL)
raw.IDPIntent.SuccessURL = successURL
raw.IDPIntent.FailureURL = failureURL
raw.IDPIntent.IDPUserID = gu.Value(raw.IDPUserID)
raw.IDPIntent.IDPUsername = gu.Value(raw.IDPUsername)
raw.IDPIntent.UserID = gu.Value(raw.UserID)
raw.IDPIntent.IDPIDToken = gu.Value(raw.IDPIDToken)
raw.IDPIntent.RequestID = gu.Value(raw.RequestID)
raw.IDPIntent.FailReason = gu.Value(raw.FailReason)
return raw.IDPIntent, nil
}
// Update implements [domain.IDPIntentRepository].
func (i idpIntentRepository) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
if changes == nil {
return 0, database.ErrNoChanges
}
if condition == nil || !condition.IsRestrictingColumn(i.InstanceIDColumn()) {
return 0, database.NewMissingConditionError(i.InstanceIDColumn())
}
if !database.Changes(changes).IsOnColumn(i.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(i.UpdatedAtColumn(), database.DefaultInstruction))
}
builder := database.StatementBuilder{}
builder.WriteString(`UPDATE zitadel.identity_provider_intents SET `)
err := database.Changes(changes).Write(&builder)
if err != nil {
return 0, err
}
writeCondition(&builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetAssertion implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetAssertion(assertion []byte) database.Change {
return database.NewChange(i.AssertionColumn(), assertion)
}
// SetFailureURL implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetFailureURL(failureURL url.URL) database.Change {
return database.NewChange(i.FailureURLColumn(), failureURL.String())
}
// SetIDPAccessToken implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetIDPAccessToken(idpAccessToken []byte) database.Change {
return database.NewChange(i.IDPAccessTokenColumn(), idpAccessToken)
}
// SetIDPIDToken implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) SetIDPIDToken(idpIDToken string) database.Change {
return database.NewChange(i.IDPIDTokenColumn(), idpIDToken)
}
// SetIDPArguments implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetIDPArguments(idpArguments []byte) database.Change {
return database.NewChange(i.IDPArgumentsColumn(), idpArguments)
}
// SetIDPEntryAttributes implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetIDPEntryAttributes(idpEntryAttributes []byte) database.Change {
return database.NewChange(i.IDPEntryAttributesColumn(), idpEntryAttributes)
}
// SetIDPID implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetIDPID(idpID string) database.Change {
return database.NewChange(i.IDPIDColumn(), idpID)
}
// SetIDPUser implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetIDPUser(idpUser []byte) database.Change {
return database.NewChange(i.IDPUserColumn(), idpUser)
}
// SetIDPUserID implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetIDPUserID(idpUserID string) database.Change {
return database.NewChange(i.IDPUserIDColumn(), idpUserID)
}
// SetIDPUsername implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetIDPUsername(idpUsername string) database.Change {
return database.NewChange(i.IDPUsernameColumn(), idpUsername)
}
// SetRequestID implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetRequestID(requestID string) database.Change {
return database.NewChange(i.RequestIDColumn(), requestID)
}
// SetState implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetState(state domain.IDPIntentState) database.Change {
return database.NewChange(i.StateColumn(), state)
}
// SetSuccessURL implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetSuccessURL(successURL url.URL) database.Change {
return database.NewChange(i.SuccessURLColumn(), successURL.String())
}
// SetUserID implements [domain.idpIntentChanges].
func (i idpIntentRepository) SetUserID(userID string) database.Change {
return database.NewChange(i.UserIDColumn(), userID)
}
// SetExpiresAt implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) SetExpiresAt(expiration time.Time) database.Change {
return database.NewChange(i.ExpiresAtColumn(), expiration)
}
// SetSucceededAt implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) SetSucceededAt(succeededAt time.Time) database.Change {
return database.NewChange(i.SucceededAtColumn(), succeededAt)
}
// SetFailedAt implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) SetFailedAt(failedAt time.Time) database.Change {
return database.NewChange(i.FailedAtColumn(), failedAt)
}
// SetFailReason implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) SetFailReason(reason string) database.Change {
return database.NewChange(i.FailReasonColumn(), reason)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// CreatedAtCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
return database.NewNumberCondition(i.CreatedAtColumn(), op, createdAt)
}
// IDCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) IDCondition(id string) database.Condition {
return database.NewTextCondition(i.IDColumn(), database.TextOperationEqual, id)
}
// IDPIDCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) IDPIDCondition(idpID string) database.Condition {
return database.NewTextCondition(i.IDPIDColumn(), database.TextOperationEqual, idpID)
}
// IDPUserIDCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) IDPUserIDCondition(idpUserID string) database.Condition {
return database.NewTextCondition(i.IDPUserIDColumn(), database.TextOperationEqual, idpUserID)
}
// IDPUsernameCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) IDPUsernameCondition(idpUsername string) database.Condition {
return database.NewTextCondition(i.IDPUsernameColumn(), database.TextOperationEqual, idpUsername)
}
// InstanceIDCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(i.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
// PrimaryKeyCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) PrimaryKeyCondition(instanceID, id string) database.Condition {
return database.And(
i.InstanceIDCondition(instanceID),
i.IDCondition(id),
)
}
// RequestIDCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) RequestIDCondition(requestID string) database.Condition {
return database.NewTextCondition(i.RequestIDColumn(), database.TextOperationEqual, requestID)
}
// StateCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) StateCondition(state domain.IDPIntentState) database.Condition {
return database.NewTextCondition(i.StateColumn(), database.TextOperationEqual, state.String())
}
// UpdatedAtCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
return database.NewNumberCondition(i.UpdatedAtColumn(), op, updatedAt)
}
// UserIDCondition implements [domain.idpIntentConditions].
func (i idpIntentRepository) UserIDCondition(userID string) database.Condition {
return database.NewTextCondition(i.UserIDColumn(), database.TextOperationEqual, userID)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// AssertionColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) AssertionColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "assertion")
}
// CreatedAtColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) CreatedAtColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "created_at")
}
// ExpiresAtColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) ExpiresAtColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "expires_at")
}
// FailureURLColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) FailureURLColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "failure_url")
}
// IDColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "id")
}
// IDPAccessTokenColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDPAccessTokenColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_access_token")
}
// IDPIDTokenColumn implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) IDPIDTokenColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_id_token")
}
// IDPArgumentsColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDPArgumentsColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_arguments")
}
// IDPEntryAttributesColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDPEntryAttributesColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_entry_attributes")
}
// IDPIDColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDPIDColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_id")
}
// IDPUserColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDPUserColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_user")
}
// IDPUserIDColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDPUserIDColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_user_id")
}
// IDPUsernameColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) IDPUsernameColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "idp_username")
}
// InstanceIDColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) InstanceIDColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "instance_id")
}
// PrimaryKeyColumns implements [domain.IDPIntentRepository].
func (i idpIntentRepository) PrimaryKeyColumns() []database.Column {
return []database.Column{i.IDColumn(), i.InstanceIDColumn()}
}
// RequestIDColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) RequestIDColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "request_id")
}
// StateColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) StateColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "state")
}
// SucceededAtColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) SucceededAtColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "succeeded_at")
}
// SuccessURLColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) SuccessURLColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "success_url")
}
// FailReasonColumn implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) FailReasonColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "fail_reason")
}
// FailedAtColumn implements [domain.IDPIntentRepository].
func (i *idpIntentRepository) FailedAtColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "failed_at")
}
// UpdatedAtColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) UpdatedAtColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "updated_at")
}
// UserIDColumn implements [domain.idpIntentColumns].
func (i idpIntentRepository) UserIDColumn() database.Column {
return database.NewColumn(i.unqualifiedTableName(), "user_id")
}
var _ domain.IDPIntentRepository = (*idpIntentRepository)(nil)
@@ -0,0 +1,591 @@
package repository_test
import (
"encoding/json"
"net/url"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
)
func TestCreateIDPIntent(t *testing.T) {
beforeCreate := time.Now()
tx, err := pool.Begin(t.Context(), nil)
require.NoError(t, err)
defer func() {
err := tx.Rollback(t.Context())
require.NoError(t, err)
}()
instanceID := createInstance(t, tx)
orgID := createOrganization(t, tx, instanceID)
idpID := createIdentityProvider(t, tx, instanceID, orgID)
successURL, err := url.Parse("https://example.com/success")
require.NoError(t, err)
failURL, err := url.Parse("https://example.com/fail")
require.NoError(t, err)
emptyURL, err := url.Parse("")
require.NoError(t, err)
intentID := gofakeit.UUID()
tt := []struct {
testName string
inputID string
inputInstanceID string
inputSuccessURL *url.URL
inputFailureURL *url.URL
inputIDPID string
inputIDPArgs map[string]any
inputCreatedAt time.Time
inputUpdatedAt time.Time
inputMaxLifetime time.Duration
expectedError error
}{
{
testName: "create and update timestamps not set / empty fail url / should generate timestamps and retrieve successfully",
inputID: intentID,
inputInstanceID: instanceID,
inputSuccessURL: successURL,
inputFailureURL: emptyURL,
inputIDPID: idpID,
inputIDPArgs: map[string]any{
"arg1": map[string]any{"k1": 1, "k2": "v2"},
"arg2": 2,
"arg3": "3",
"arg4": true,
},
inputMaxLifetime: time.Hour * 2,
},
{
testName: "all set / should retrieve successfully",
inputID: intentID,
inputInstanceID: instanceID,
inputSuccessURL: successURL,
inputFailureURL: failURL,
inputIDPID: idpID,
inputIDPArgs: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}},
inputMaxLifetime: time.Hour * 2,
inputCreatedAt: time.Now(),
inputUpdatedAt: time.Now(),
},
{
testName: "id not set / should return check error",
inputCreatedAt: time.Now(),
inputUpdatedAt: time.Now(),
inputInstanceID: instanceID,
inputSuccessURL: successURL,
inputFailureURL: emptyURL,
inputIDPID: idpID,
inputIDPArgs: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}},
expectedError: database.NewCheckError("", "", nil),
},
{
testName: "instance id not found / should return foreign key error",
inputID: intentID,
inputCreatedAt: time.Now(),
inputUpdatedAt: time.Now(),
inputSuccessURL: successURL,
inputFailureURL: emptyURL,
inputIDPID: idpID,
inputIDPArgs: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}},
expectedError: database.NewForeignKeyError("", "", nil),
},
{
testName: "idp id not found / should return foreign key error",
inputID: intentID,
inputInstanceID: instanceID,
inputCreatedAt: time.Now(),
inputUpdatedAt: time.Now(),
inputSuccessURL: successURL,
inputFailureURL: failURL,
inputIDPArgs: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}},
expectedError: database.NewForeignKeyError("", "", nil),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Given
savePoint, savePointErr := tx.Begin(t.Context())
require.NoError(t, savePointErr)
defer func() {
err := savePoint.Rollback(t.Context())
require.NoError(t, err)
}()
idpIntentRepo := repository.IDPIntentRepository()
intent := domain.IDPIntent{
ID: tc.inputID,
InstanceID: tc.inputInstanceID,
SuccessURL: tc.inputSuccessURL,
FailureURL: tc.inputFailureURL,
IDPID: tc.inputIDPID,
IDPArguments: tc.inputIDPArgs,
State: domain.IDPIntentStateFailed, // Should disregard
IDPUserID: "should disregard",
}
// Test
createErr := idpIntentRepo.Create(t.Context(), tx, &intent)
afterCreate := time.Now()
// Verify
assert.ErrorIs(t, createErr, tc.expectedError)
if tc.expectedError == nil {
retrieved, getErr := idpIntentRepo.Get(t.Context(), tx, database.WithCondition(idpIntentRepo.PrimaryKeyCondition(instanceID, intentID)))
require.NoError(t, getErr)
require.NotNil(t, retrieved)
assert.WithinRange(t, retrieved.CreatedAt, beforeCreate, afterCreate)
assert.WithinRange(t, retrieved.UpdatedAt, beforeCreate, afterCreate)
assert.NotZero(t, retrieved.ID)
if tc.inputID != "" {
assert.Equal(t, tc.inputID, retrieved.ID)
}
assert.Equal(t, tc.inputInstanceID, retrieved.InstanceID)
assert.Equal(t, tc.inputSuccessURL, retrieved.SuccessURL)
assert.Equal(t, tc.inputFailureURL, retrieved.FailureURL)
assert.Equal(t, tc.inputIDPID, retrieved.IDPID)
assert.NotZero(t, retrieved.IDPArguments)
assert.Equal(t, domain.IDPIntentStateStarted, retrieved.State)
assert.Zero(t, retrieved.IDPUserID)
}
})
}
}
func TestGetIDPIntent(t *testing.T) {
tx, err := pool.Begin(t.Context(), nil)
require.NoError(t, err)
defer func() {
err := tx.Rollback(t.Context())
require.NoError(t, err)
}()
instanceID := createInstance(t, tx)
orgID := createOrganization(t, tx, instanceID)
idpID := createIdentityProvider(t, tx, instanceID, orgID)
idpIntentRepo := repository.IDPIntentRepository()
intentID1 := createIDPIntent(t, tx, instanceID, idpID)
successURL, err := url.Parse("https://example.com/success")
require.NoError(t, err)
failURL, err := url.Parse("https://example.com/fail")
require.NoError(t, err)
intent2 := domain.IDPIntent{
ID: gofakeit.UUID(),
InstanceID: instanceID,
SuccessURL: successURL,
FailureURL: failURL,
IDPID: idpID,
IDPArguments: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}},
CreatedAt: time.Now().AddDate(0, 0, 1),
}
err = idpIntentRepo.Create(t.Context(), tx, &intent2)
require.NoError(t, err)
tt := []struct {
testName string
inputQueryOpts []database.QueryOption
expectedError error
expectedIntentID string
}{
{
testName: "when no condition set should return missing condition error",
expectedError: database.NewMissingConditionError(nil),
},
{
testName: "when no instance condition set should return missing condition error",
inputQueryOpts: []database.QueryOption{
database.WithCondition(idpIntentRepo.IDCondition(intentID1)),
},
expectedError: database.NewMissingConditionError(nil),
},
{
testName: "when primary key condition set should return matching intent",
inputQueryOpts: []database.QueryOption{
database.WithCondition(idpIntentRepo.PrimaryKeyCondition(instanceID, intentID1)),
},
expectedIntentID: intentID1,
},
{
testName: "when filtering by creation date descending should return matching intent with highest PK",
inputQueryOpts: []database.QueryOption{
database.WithCondition(database.And(
idpIntentRepo.InstanceIDCondition(instanceID),
)),
database.WithOrderByDescending(idpIntentRepo.CreatedAtColumn()),
database.WithLimit(1),
},
expectedIntentID: intent2.ID,
},
{
testName: "when filtering by non-existent PK should return no row found error",
inputQueryOpts: []database.QueryOption{
database.WithCondition(database.And(
idpIntentRepo.InstanceIDCondition(instanceID),
idpIntentRepo.IDCondition("not existing"),
)),
},
expectedError: database.NewNoRowFoundError(nil),
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
// Test
retrieved, err := idpIntentRepo.Get(t.Context(), tx, tc.inputQueryOpts...)
// Verify
assert.ErrorIs(t, err, tc.expectedError)
if tc.expectedError == nil {
assert.Equal(t, tc.expectedIntentID, retrieved.ID)
assert.Equal(t, "https://example.com/success", retrieved.SuccessURL.String())
assert.Equal(t, "https://example.com/fail", retrieved.FailureURL.String())
}
})
}
}
func TestUpdateIDPIntent(t *testing.T) {
beforeUpdate := time.Now()
tx, err := pool.Begin(t.Context(), nil)
require.NoError(t, err)
defer func() {
err := tx.Rollback(t.Context())
require.NoError(t, err)
}()
instanceID := createInstance(t, tx)
orgID := createOrganization(t, tx, instanceID)
idpID := createIdentityProvider(t, tx, instanceID, orgID)
idpIntentRepo := repository.IDPIntentRepository()
successURL, err := url.Parse("https://example.com/success")
require.NoError(t, err)
failURL, err := url.Parse("https://example.com/fail")
require.NoError(t, err)
intentID1 := gofakeit.UUID()
intent1 := domain.IDPIntent{
ID: intentID1,
InstanceID: instanceID,
SuccessURL: successURL,
FailureURL: failURL,
IDPID: idpID,
IDPArguments: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}, "arg2": []int32{1}},
CreatedAt: time.Now(),
}
err = idpIntentRepo.Create(t.Context(), tx, &intent1)
require.NoError(t, err)
entryAttrs := map[string][]string{
"attr1": {"1", "2"},
"attr2": {"3", "4"},
}
marshalledAttrs, marshallErr := json.Marshal(entryAttrs)
require.NoError(t, marshallErr)
intentID2 := gofakeit.UUID()
intent2 := domain.IDPIntent{
ID: intentID2,
InstanceID: instanceID,
SuccessURL: successURL,
FailureURL: failURL,
IDPID: idpID,
IDPArguments: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}},
CreatedAt: time.Now(),
}
err = idpIntentRepo.Create(t.Context(), tx, &intent2)
require.NoError(t, err)
now := time.Now()
tomorrow := now.AddDate(0, 0, 1)
tt := []struct {
testName string
inputConditions database.Condition
inputChanges []database.Change
expectedError error
expectedUpdatedRecords func() []*domain.IDPIntent
expectedUpdatedRows int64
}{
{
testName: "when no changes should return no changes error",
expectedError: database.ErrNoChanges,
},
{
testName: "when no condition set should return missing condition error",
inputChanges: database.Changes{
idpIntentRepo.SetIDPUser([]byte("not a user")),
},
expectedError: database.NewMissingConditionError(idpIntentRepo.InstanceIDColumn()),
},
{
testName: "when simulating SucceededEvent should update fields accordingly",
inputConditions: idpIntentRepo.PrimaryKeyCondition(instanceID, intentID1),
inputChanges: database.Changes{
idpIntentRepo.SetState(domain.IDPIntentStateSucceeded),
idpIntentRepo.SetIDPUser([]byte("update user")),
idpIntentRepo.SetIDPUserID("idp user id updated"),
idpIntentRepo.SetIDPUsername("idp username updated"),
idpIntentRepo.SetUserID("some-user-id"),
idpIntentRepo.SetIDPAccessToken([]byte(`{"secret": "g1g4 s3cr3t 4cc3ss t0k3n"}`)),
idpIntentRepo.SetIDPIDToken("idp id token"),
idpIntentRepo.SetSucceededAt(now),
idpIntentRepo.SetExpiresAt(tomorrow),
},
expectedUpdatedRecords: func() []*domain.IDPIntent {
toReturn := intent1
toReturn.IDPUser = []byte("update user")
toReturn.IDPUserID = "idp user id updated"
toReturn.IDPUsername = "idp username updated"
toReturn.UserID = "some-user-id"
toReturn.IDPAccessToken = []byte(`{"secret": "g1g4 s3cr3t 4cc3ss t0k3n"}`)
toReturn.IDPIDToken = "idp id token"
toReturn.SucceededAt = &now
toReturn.ExpiresAt = &tomorrow
toReturn.State = domain.IDPIntentStateSucceeded
return []*domain.IDPIntent{&toReturn}
},
expectedUpdatedRows: 1,
},
{
testName: "when simulating SAMLSucceededEvent should update fields accordingly",
inputConditions: idpIntentRepo.PrimaryKeyCondition(instanceID, intentID1),
inputChanges: database.Changes{
idpIntentRepo.SetState(domain.IDPIntentStateSucceeded),
idpIntentRepo.SetIDPUser([]byte("update user")),
idpIntentRepo.SetIDPUserID("idp user id updated"),
idpIntentRepo.SetIDPUsername("idp username updated"),
idpIntentRepo.SetUserID("some-user-id"),
idpIntentRepo.SetAssertion([]byte(`{"assertion": "val1"}`)),
idpIntentRepo.SetSucceededAt(now),
idpIntentRepo.SetExpiresAt(tomorrow),
},
expectedUpdatedRecords: func() []*domain.IDPIntent {
toReturn := intent1
toReturn.IDPUser = []byte("update user")
toReturn.IDPUserID = "idp user id updated"
toReturn.IDPUsername = "idp username updated"
toReturn.UserID = "some-user-id"
toReturn.Assertion = []byte(`{"assertion": "val1"}`)
toReturn.SucceededAt = &now
toReturn.ExpiresAt = &tomorrow
toReturn.State = domain.IDPIntentStateSucceeded
return []*domain.IDPIntent{&toReturn}
},
expectedUpdatedRows: 1,
},
{
testName: "when simulating LDAPSucceededEvent should update fields accordingly",
inputConditions: idpIntentRepo.PrimaryKeyCondition(instanceID, intentID1),
inputChanges: database.Changes{
idpIntentRepo.SetState(domain.IDPIntentStateSucceeded),
idpIntentRepo.SetIDPUser([]byte("update user")),
idpIntentRepo.SetIDPUserID("idp user id updated"),
idpIntentRepo.SetIDPUsername("idp username updated"),
idpIntentRepo.SetUserID("some-user-id"),
idpIntentRepo.SetIDPEntryAttributes(marshalledAttrs),
idpIntentRepo.SetSucceededAt(now),
idpIntentRepo.SetExpiresAt(tomorrow),
},
expectedUpdatedRecords: func() []*domain.IDPIntent {
toReturn := intent1
toReturn.IDPUser = []byte("update user")
toReturn.IDPUserID = "idp user id updated"
toReturn.IDPUsername = "idp username updated"
toReturn.UserID = "some-user-id"
toReturn.EntryAttributes = entryAttrs
toReturn.SucceededAt = &now
toReturn.ExpiresAt = &tomorrow
toReturn.State = domain.IDPIntentStateSucceeded
return []*domain.IDPIntent{&toReturn}
},
expectedUpdatedRows: 1,
},
{
testName: "when simulating FailedEvent should update reason, state and failedAt",
inputConditions: idpIntentRepo.PrimaryKeyCondition(instanceID, intentID1),
inputChanges: database.Changes{
idpIntentRepo.SetState(domain.IDPIntentStateFailed),
idpIntentRepo.SetFailReason("mock failure"),
idpIntentRepo.SetFailedAt(now),
},
expectedUpdatedRecords: func() []*domain.IDPIntent {
toReturn := intent1
toReturn.FailReason = "mock failure"
toReturn.State = domain.IDPIntentStateFailed
toReturn.FailedAt = &now
return []*domain.IDPIntent{&toReturn}
},
expectedUpdatedRows: 1,
},
{
testName: "when simulating SAMLRequestEvent should update request ID",
inputConditions: idpIntentRepo.PrimaryKeyCondition(instanceID, intentID1),
inputChanges: database.Changes{
idpIntentRepo.SetRequestID("req-123"),
},
expectedUpdatedRecords: func() []*domain.IDPIntent {
toReturn := intent1
toReturn.RequestID = "req-123"
return []*domain.IDPIntent{&toReturn}
},
expectedUpdatedRows: 1,
},
{
testName: "when updating multiple records should return updated records",
inputConditions: database.Or(
idpIntentRepo.PrimaryKeyCondition(instanceID, intentID1),
idpIntentRepo.PrimaryKeyCondition(instanceID, intentID2),
),
inputChanges: database.Changes{
idpIntentRepo.SetState(domain.IDPIntentStateFailed),
idpIntentRepo.SetRequestID("req-123"),
idpIntentRepo.SetFailedAt(now),
},
expectedUpdatedRecords: func() []*domain.IDPIntent {
toReturn1, toReturn2 := intent1, intent2
toReturn1.State = domain.IDPIntentStateFailed
toReturn1.RequestID = "req-123"
toReturn1.FailedAt = &now
toReturn2.State = domain.IDPIntentStateFailed
toReturn2.RequestID = "req-123"
toReturn2.FailedAt = &now
return []*domain.IDPIntent{&toReturn1, &toReturn2}
},
expectedUpdatedRows: 2,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
savePoint, err := tx.Begin(t.Context())
require.NoError(t, err)
defer func() {
err := savePoint.Rollback(t.Context())
require.NoError(t, err)
}()
// Test
updatedCount, err := idpIntentRepo.Update(t.Context(), savePoint, tc.inputConditions, tc.inputChanges...)
afterUpdate := time.Now()
// Verify
assert.ErrorIs(t, err, tc.expectedError)
require.Equal(t, tc.expectedUpdatedRows, updatedCount)
if tc.expectedError == nil {
for _, expectedIntent := range tc.expectedUpdatedRecords() {
retrievedIntent, err := idpIntentRepo.Get(t.Context(), savePoint, database.WithCondition(idpIntentRepo.PrimaryKeyCondition(instanceID, expectedIntent.ID)))
require.NoError(t, err)
assert.Equal(t, expectedIntent.ID, retrievedIntent.ID)
assert.Equal(t, expectedIntent.InstanceID, retrievedIntent.InstanceID)
assert.Equal(t, expectedIntent.State, retrievedIntent.State)
assert.Equal(t, expectedIntent.SuccessURL, retrievedIntent.SuccessURL)
assert.Equal(t, expectedIntent.FailureURL, retrievedIntent.FailureURL)
assert.Equal(t, expectedIntent.CreatedAt, retrievedIntent.CreatedAt)
assert.WithinRange(t, retrievedIntent.UpdatedAt, beforeUpdate, afterUpdate)
assert.Equal(t, expectedIntent.IDPID, retrievedIntent.IDPID)
assert.Equal(t, expectedIntent.IDPUser, retrievedIntent.IDPUser)
assert.Equal(t, expectedIntent.IDPUserID, retrievedIntent.IDPUserID)
assert.Equal(t, expectedIntent.IDPUsername, retrievedIntent.IDPUsername)
assert.Equal(t, expectedIntent.UserID, retrievedIntent.UserID)
assert.Equal(t, expectedIntent.IDPAccessToken, retrievedIntent.IDPAccessToken)
assert.Equal(t, expectedIntent.IDPIDToken, retrievedIntent.IDPIDToken)
assert.Equal(t, expectedIntent.EntryAttributes, retrievedIntent.EntryAttributes)
assert.Equal(t, expectedIntent.RequestID, retrievedIntent.RequestID)
assert.Equal(t, expectedIntent.Assertion, retrievedIntent.Assertion)
if expectedIntent.State == domain.IDPIntentStateSucceeded {
require.NotNil(t, retrievedIntent.SucceededAt)
assert.WithinRange(t, *retrievedIntent.SucceededAt, beforeUpdate, afterUpdate)
require.NotNil(t, retrievedIntent.ExpiresAt)
assert.WithinRange(t, *retrievedIntent.ExpiresAt, beforeUpdate.AddDate(0, 0, 1), afterUpdate.AddDate(0, 0, 1))
}
if expectedIntent.State == domain.IDPIntentStateFailed {
require.NotNil(t, retrievedIntent.FailedAt)
assert.WithinRange(t, *retrievedIntent.FailedAt, beforeUpdate, afterUpdate)
}
assert.Equal(t, expectedIntent.FailReason, retrievedIntent.FailReason)
}
}
})
}
}
func TestDeleteIDPIntent(t *testing.T) {
tx, err := pool.Begin(t.Context(), nil)
require.NoError(t, err)
defer func() {
err := tx.Rollback(t.Context())
require.NoError(t, err)
}()
instanceID := createInstance(t, tx)
orgID := createOrganization(t, tx, instanceID)
idpID := createIdentityProvider(t, tx, instanceID, orgID)
intentID := createIDPIntent(t, tx, instanceID, idpID)
otherIntentID := createIDPIntent(t, tx, instanceID, idpID)
createIDPIntent(t, tx, instanceID, idpID)
idpIntentRepo := repository.IDPIntentRepository()
tt := []struct {
testName string
inputConditions database.Condition
expectedError error
expectedDeletedCount int64
}{
{
testName: "when PK condition not set should return missing condition error",
inputConditions: idpIntentRepo.StateCondition(domain.IDPIntentStateFailed),
expectedError: database.NewMissingConditionError(nil),
},
{
testName: "when condition matches record should delete matching record",
inputConditions: idpIntentRepo.PrimaryKeyCondition(instanceID, intentID),
expectedDeletedCount: 1,
},
{
testName: "when conditions matches record should delete matching records",
inputConditions: database.Or(
idpIntentRepo.PrimaryKeyCondition(instanceID, intentID),
idpIntentRepo.PrimaryKeyCondition(instanceID, otherIntentID),
),
expectedDeletedCount: 2,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
savePoint, err := tx.Begin(t.Context())
require.NoError(t, err)
defer func() {
err := savePoint.Rollback(t.Context())
require.NoError(t, err)
}()
// Test
deleteCount, err := idpIntentRepo.Delete(t.Context(), savePoint, tc.inputConditions)
// Verify
assert.ErrorIs(t, err, tc.expectedError)
assert.Equal(t, tc.expectedDeletedCount, deleteCount)
})
}
}
@@ -4,10 +4,13 @@ import (
"context"
"fmt"
"log"
"net/url"
"os"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/backend/v3/domain"
@@ -161,3 +164,49 @@ func createProjectGrant(t *testing.T, tx database.Transaction, instanceID, grant
return projectGrant.ID
}
func createIdentityProvider(t *testing.T, tx database.Transaction, instanceID, orgID string) string {
t.Helper()
idp := domain.IdentityProvider{
InstanceID: instanceID,
OrgID: &orgID,
ID: gofakeit.UUID(),
State: domain.IDPStateActive,
Name: gofakeit.Name(),
Type: gu.Ptr(domain.IDPTypeOIDC),
AllowCreation: true,
AllowAutoCreation: true,
AllowAutoUpdate: true,
AllowLinking: true,
StylingType: &stylingType,
Payload: []byte("{}"),
}
idpRepo := repository.IDProviderRepository()
err := idpRepo.Create(t.Context(), tx, &idp)
require.NoError(t, err)
return idp.ID
}
func createIDPIntent(t *testing.T, tx database.Transaction, instanceID, idpID string) string {
t.Helper()
successURL, err := url.Parse("https://example.com/success")
require.NoError(t, err)
failURL, err := url.Parse("https://example.com/fail")
require.NoError(t, err)
intent := domain.IDPIntent{
ID: gofakeit.UUID(),
InstanceID: instanceID,
SuccessURL: successURL,
FailureURL: failURL,
IDPID: idpID,
IDPArguments: map[string]any{"arg1": map[string]any{"k1": 1, "k2": "v2"}},
CreatedAt: time.Now(),
}
idpIntentRepo := repository.IDPIntentRepository()
err = idpIntentRepo.Create(t.Context(), tx, &intent)
require.NoError(t, err)
return intent.ID
}
@@ -365,9 +365,9 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", "", expiry)
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", "", expiry)
require.NoError(t, err)
jwtSuccessfulID, jwtToken, jwtChangeDate, jwtSequence, err := sink.SuccessfulJWTIntent(Instance.ID(), jwtIdPID, "id", "", expiry)
@@ -2245,9 +2245,9 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", "", expiry)
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", "", expiry)
require.NoError(t, err)
type args struct {
ctx context.Context
+60
View File
@@ -0,0 +1,60 @@
//go:build !integration
package ldap
import (
"context"
"errors"
"github.com/go-ldap/ldap/v3"
"github.com/zitadel/zitadel/internal/idp"
)
// FetchUser implements the [idp.Session] interface.
func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) {
var user *ldap.Entry
for _, server := range s.Provider.servers {
user, err = tryBind(server,
s.Provider.startTLS,
s.Provider.bindDN,
s.Provider.bindPassword,
s.Provider.baseDN,
s.Provider.getNecessaryAttributes(),
s.Provider.userObjectClasses,
s.Provider.userFilters,
s.User,
s.Password,
s.Provider.timeout,
s.Provider.rootCA)
// If there were invalid credentials or multiple users with the credentials cancel process
if err != nil && (errors.Is(err, ErrFailedLogin) || errors.Is(err, ErrNoSingleUser)) {
return nil, err
}
// If a user bind was successful and user is filled continue with login, otherwise try next server
if err == nil && user != nil {
break
}
}
if err != nil {
return nil, err
}
s.Entry = user
return mapLDAPEntryToUser(
user,
s.Provider.idAttribute,
s.Provider.firstNameAttribute,
s.Provider.lastNameAttribute,
s.Provider.displayNameAttribute,
s.Provider.nickNameAttribute,
s.Provider.preferredUsernameAttribute,
s.Provider.emailAttribute,
s.Provider.emailVerifiedAttribute,
s.Provider.phoneAttribute,
s.Provider.phoneVerifiedAttribute,
s.Provider.preferredLanguageAttribute,
s.Provider.avatarURLAttribute,
s.Provider.profileAttribute,
)
}
@@ -0,0 +1,68 @@
//go:build integration
package ldap
import (
"context"
"github.com/brianvoe/gofakeit/v6"
"github.com/go-ldap/ldap/v3"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
const (
FailingUser = "IAM FAIL"
FailingPsw = "test failed successfully"
)
// FetchUser implements the [idp.Session] interface.
//
// This is a mock implementation of [idp.Session.FetchUser] that is used
// during integration tests and substitutes the real [idp.Session.FetchUser]
// implementation.
//
// This has been done to allow writing integration tests against a
// fake (external) LDAP server.
//
// The substitution/overloading is done through build tags.
//
// When the LDAP start intent is called with `username = [FailingUser]` and
// `password = [FailingPsw]`, the method return [ErrFailedLogin] to be able
// to mock an intent failed event.
func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) {
if s.User == FailingUser && s.Password == FailingPsw {
return nil, ErrFailedLogin
}
usr := NewUser(
gofakeit.UUID(),
gofakeit.FirstName(),
gofakeit.LastName(),
gofakeit.Name(),
gofakeit.Name(),
gofakeit.Name(),
domain.EmailAddress(gofakeit.Email()),
true,
domain.PhoneNumber(gofakeit.Phone()),
true,
language.English,
gofakeit.URL(),
gofakeit.UUID(),
)
s.Entry = &ldap.Entry{
DN: s.Provider.baseDN,
Attributes: []*ldap.EntryAttribute{
{Name: "uid", Values: []string{gofakeit.UUID()}},
{Name: "givenName", Values: []string{gofakeit.FirstName()}},
{Name: "sn", Values: []string{gofakeit.LastName()}},
{Name: "mail", Values: []string{gofakeit.Email()}},
{Name: "telephoneNumber", Values: []string{gofakeit.Phone()}},
{Name: "displayName", Values: []string{gofakeit.Name()}},
},
}
return usr, nil
}
+1
View File
@@ -245,6 +245,7 @@ func (p *Provider) IsAutoUpdate() bool {
return p.isAutoUpdate
}
// Do not remove, used in [Session.FetchUser] @ fetch_user.go
func (p *Provider) getNecessaryAttributes() []string {
attributes := []string{p.userBase}
if p.idAttribute != "" {
+1 -48
View File
@@ -48,58 +48,11 @@ func (s *Session) PersistentParameters() map[string]any {
return nil
}
// FetchUser implements the [idp.Session] interface.
func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) {
var user *ldap.Entry
for _, server := range s.Provider.servers {
user, err = tryBind(server,
s.Provider.startTLS,
s.Provider.bindDN,
s.Provider.bindPassword,
s.Provider.baseDN,
s.Provider.getNecessaryAttributes(),
s.Provider.userObjectClasses,
s.Provider.userFilters,
s.User,
s.Password,
s.Provider.timeout,
s.Provider.rootCA)
// If there were invalid credentials or multiple users with the credentials cancel process
if err != nil && (errors.Is(err, ErrFailedLogin) || errors.Is(err, ErrNoSingleUser)) {
return nil, err
}
// If a user bind was successful and user is filled continue with login, otherwise try next server
if err == nil && user != nil {
break
}
}
if err != nil {
return nil, err
}
s.Entry = user
return mapLDAPEntryToUser(
user,
s.Provider.idAttribute,
s.Provider.firstNameAttribute,
s.Provider.lastNameAttribute,
s.Provider.displayNameAttribute,
s.Provider.nickNameAttribute,
s.Provider.preferredUsernameAttribute,
s.Provider.emailAttribute,
s.Provider.emailVerifiedAttribute,
s.Provider.phoneAttribute,
s.Provider.phoneVerifiedAttribute,
s.Provider.preferredLanguageAttribute,
s.Provider.avatarURLAttribute,
s.Provider.profileAttribute,
)
}
func (s *Session) ExpiresAt() time.Time {
return time.Time{} // falls back to the default expiration time
}
// Do not remove, used in [Session.FetchUser] @ fetch_user.go
func tryBind(
server string,
startTLS bool,
+13 -2
View File
@@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"path"
"strings"
"sync"
"sync/atomic"
"time"
@@ -108,7 +109,12 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry ti
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
}
func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
// SuccessfulSAMLIntent creates a SAMLIntent and then executes the flow to put it in
// successful state.
//
// If intentID is not empty, the intent creation is skipped and only the flow part to switch
// the intent to a successful state is executed.
func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID, intentID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
@@ -120,6 +126,7 @@ func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry ti
IDPUserID: idpUserID,
UserID: userID,
Expiry: expiry,
IntentID: intentID,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
@@ -341,6 +348,7 @@ type SuccessfulIntentRequest struct {
IDPUserID string `json:"idp_user_id"`
UserID string `json:"user_id"`
Expiry time.Time `json:"expiry"`
IntentID string `json:"intent_id"`
}
type SuccessfulIntentResponse struct {
IntentID string `json:"intent_id"`
@@ -519,7 +527,10 @@ func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req
}
func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) {
intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID)
intentID := strings.TrimSpace(req.IntentID)
if intentID == "" {
intentID, _ = createIntent(ctx, cmd, req.InstanceID, req.IDPID)
}
writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID)
idpUser := &saml.UserMapper{
@@ -0,0 +1,257 @@
package projection
// TODO(adlerhurst): Uncomment once users table is created and FK can be enforced
// import (
// "context"
// "database/sql"
// "encoding/json"
// "github.com/zitadel/zitadel/backend/v3/domain"
// "github.com/zitadel/zitadel/backend/v3/storage/database"
// v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
// "github.com/zitadel/zitadel/internal/eventstore"
// "github.com/zitadel/zitadel/internal/eventstore/handler/v2"
// "github.com/zitadel/zitadel/internal/repository/idpintent"
// "github.com/zitadel/zitadel/internal/zerrors"
// )
// const (
// IDPIntentRelationalProjectionTable = "zitadel.identity_provider_intents"
// )
// type idpIntentRelationalProjection struct{}
// func (*idpIntentRelationalProjection) Name() string {
// return IDPIntentRelationalProjectionTable
// }
// func newIDPIntentRelationalProjection(ctx context.Context, config handler.Config) *handler.Handler {
// return handler.NewHandler(ctx, &config, new(idpIntentRelationalProjection))
// }
// // Reducers implements [handler.Projection].
// func (i *idpIntentRelationalProjection) Reducers() []handler.AggregateReducer {
// return []handler.AggregateReducer{
// {
// Aggregate: idpintent.AggregateType,
// EventReducers: []handler.EventReducer{
// {Event: idpintent.StartedEventType, Reduce: i.reduceStartedEvent},
// {Event: idpintent.SucceededEventType, Reduce: i.reduceSucceededEvent},
// {Event: idpintent.SAMLSucceededEventType, Reduce: i.reduceSAMLSucceededEvent},
// {Event: idpintent.SAMLRequestEventType, Reduce: i.reduceSAMLRequestEvent},
// {Event: idpintent.LDAPSucceededEventType, Reduce: i.reduceLDAPSucceededEvent},
// {Event: idpintent.FailedEventType, Reduce: i.reduceFailedEvent},
// {Event: idpintent.ConsumedEventType, Reduce: i.reduceConsumedEvent},
// },
// },
// }
// }
// func (i *idpIntentRelationalProjection) reduceStartedEvent(evt eventstore.Event) (*handler.Statement, error) {
// e, ok := evt.(*idpintent.StartedEvent)
// if !ok {
// return nil, zerrors.ThrowInternalf(nil, "HANDL-Lqj3HB", "reduce.wrong.event.type %s", idpintent.StartedEventType)
// }
// return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
// tx, ok := ex.(*sql.Tx)
// if !ok {
// return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rirFQm", "reduce.wrong.db.pool %T", ex)
// }
// repo := repository.IDPIntentRepository()
// return repo.Create(ctx, v3_sql.SQLTx(tx), &domain.IDPIntent{
// ID: e.Aggregate().ID,
// InstanceID: e.Aggregate().InstanceID,
// State: domain.IDPIntentStateStarted,
// SuccessURL: e.SuccessURL,
// FailureURL: e.FailureURL,
// CreatedAt: e.CreationDate(),
// UpdatedAt: e.CreationDate(),
// IDPID: e.IDPID,
// IDPArguments: e.IDPArguments,
// })
// }), nil
// }
// func (i *idpIntentRelationalProjection) reduceSucceededEvent(evt eventstore.Event) (*handler.Statement, error) {
// e, ok := evt.(*idpintent.SucceededEvent)
// if !ok {
// return nil, zerrors.ThrowInternalf(nil, "HANDL-hQSxj0", "reduce.wrong.event.type %s", idpintent.SucceededEventType)
// }
// return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
// tx, ok := ex.(*sql.Tx)
// if !ok {
// return zerrors.ThrowInvalidArgumentf(nil, "HANDL-ry4tFH", "reduce.wrong.db.pool %T", ex)
// }
// idpAccessToken, err := e.IDPAccessToken.Value()
// if err != nil {
// return err
// }
// jsonIDPAccessToken, ok := idpAccessToken.([]byte)
// if !ok {
// return zerrors.ThrowInternal(nil, "HANDL-32XE1L", "reduce.convertion.json.failed")
// }
// repo := repository.IDPIntentRepository()
// changes := []database.Change{
// repo.SetState(domain.IDPIntentStateSucceeded),
// repo.SetIDPUser(e.IDPUser),
// repo.SetIDPUserID(e.IDPUserID),
// repo.SetIDPUsername(e.IDPUserName),
// repo.SetUserID(e.UserID),
// repo.SetIDPAccessToken(jsonIDPAccessToken),
// repo.SetIDPIDToken(e.IDPIDToken),
// repo.SetSucceededAt(e.CreationDate()),
// repo.SetExpiresAt(e.ExpiresAt),
// }
// _, err = repo.Update(
// ctx,
// v3_sql.SQLTx(tx),
// repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
// changes...,
// )
// return err
// }), nil
// }
// func (i *idpIntentRelationalProjection) reduceSAMLSucceededEvent(evt eventstore.Event) (*handler.Statement, error) {
// e, ok := evt.(*idpintent.SAMLSucceededEvent)
// if !ok {
// return nil, zerrors.ThrowInternalf(nil, "HANDL-t2uaiv", "reduce.wrong.event.type %s", idpintent.SAMLSucceededEventType)
// }
// return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
// tx, ok := ex.(*sql.Tx)
// if !ok {
// return zerrors.ThrowInvalidArgumentf(nil, "HANDL-0QGPXm", "reduce.wrong.db.pool %T", ex)
// }
// assertion, err := e.Assertion.Value()
// if err != nil {
// return err
// }
// jsonAssertion, ok := assertion.([]byte)
// if !ok {
// return zerrors.ThrowInternal(nil, "HANDL-1TFjTi", "reduce.convertion.json.failed")
// }
// repo := repository.IDPIntentRepository()
// _, err = repo.Update(
// ctx,
// v3_sql.SQLTx(tx),
// repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
// repo.SetState(domain.IDPIntentStateSucceeded),
// repo.SetIDPUser(e.IDPUser),
// repo.SetIDPUserID(e.IDPUserID),
// repo.SetIDPUsername(e.IDPUserName),
// repo.SetUserID(e.UserID),
// repo.SetAssertion(jsonAssertion),
// repo.SetSucceededAt(e.CreationDate()),
// repo.SetExpiresAt(e.ExpiresAt),
// )
// return err
// }), nil
// }
// func (i *idpIntentRelationalProjection) reduceSAMLRequestEvent(evt eventstore.Event) (*handler.Statement, error) {
// e, ok := evt.(*idpintent.SAMLRequestEvent)
// if !ok {
// return nil, zerrors.ThrowInternalf(nil, "HANDL-f7O6K2", "reduce.wrong.event.type %s", idpintent.SAMLRequestEventType)
// }
// return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
// tx, ok := ex.(*sql.Tx)
// if !ok {
// return zerrors.ThrowInvalidArgumentf(nil, "HANDL-ijgAPJ", "reduce.wrong.db.pool %T", ex)
// }
// repo := repository.IDPIntentRepository()
// _, err := repo.Update(
// ctx,
// v3_sql.SQLTx(tx),
// repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
// repo.SetRequestID(e.RequestID),
// )
// return err
// }), nil
// }
// func (i *idpIntentRelationalProjection) reduceLDAPSucceededEvent(evt eventstore.Event) (*handler.Statement, error) {
// e, ok := evt.(*idpintent.LDAPSucceededEvent)
// if !ok {
// return nil, zerrors.ThrowInternalf(nil, "HANDL-Gw8ddW", "reduce.wrong.event.type %s", idpintent.LDAPSucceededEventType)
// }
// return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
// tx, ok := ex.(*sql.Tx)
// if !ok {
// return zerrors.ThrowInvalidArgumentf(nil, "HANDL-tNp8aa", "reduce.wrong.db.pool %T", ex)
// }
// jsonAttributes, err := json.Marshal(e.EntryAttributes)
// if err != nil {
// return zerrors.ThrowInternal(nil, "HANDL-LuvCvD", "reduce.convertion.json.failed")
// }
// repo := repository.IDPIntentRepository()
// _, err = repo.Update(
// ctx,
// v3_sql.SQLTx(tx),
// repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
// repo.SetState(domain.IDPIntentStateSucceeded),
// repo.SetIDPUser(e.IDPUser),
// repo.SetIDPUserID(e.IDPUserID),
// repo.SetIDPUsername(e.IDPUserName),
// repo.SetUserID(e.UserID),
// repo.SetIDPEntryAttributes(jsonAttributes),
// repo.SetSucceededAt(e.CreationDate()),
// repo.SetExpiresAt(e.ExpiresAt),
// )
// return err
// }), nil
// }
// func (i *idpIntentRelationalProjection) reduceFailedEvent(evt eventstore.Event) (*handler.Statement, error) {
// e, ok := evt.(*idpintent.FailedEvent)
// if !ok {
// return nil, zerrors.ThrowInternalf(nil, "HANDL-x3o9LZ", "reduce.wrong.event.type %s", idpintent.FailedEventType)
// }
// return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
// tx, ok := ex.(*sql.Tx)
// if !ok {
// return zerrors.ThrowInvalidArgumentf(nil, "HANDL-iqmR8O", "reduce.wrong.db.pool %T", ex)
// }
// repo := repository.IDPIntentRepository()
// _, err := repo.Update(
// ctx,
// v3_sql.SQLTx(tx),
// repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
// repo.SetState(domain.IDPIntentStateFailed),
// repo.SetFailReason(e.Reason),
// repo.SetFailedAt(e.CreatedAt()),
// )
// return err
// }), nil
// }
// func (i *idpIntentRelationalProjection) reduceConsumedEvent(evt eventstore.Event) (*handler.Statement, error) {
// e, ok := evt.(*idpintent.ConsumedEvent)
// if !ok {
// return nil, zerrors.ThrowInternalf(nil, "HANDL-jHZs6T", "reduce.wrong.event.type %s", idpintent.ConsumedEventType)
// }
// return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
// tx, ok := ex.(*sql.Tx)
// if !ok {
// return zerrors.ThrowInvalidArgumentf(nil, "HANDL-8X9U9w", "reduce.wrong.db.pool %T", ex)
// }
// repo := repository.IDPIntentRepository()
// _, err := repo.Delete(
// ctx,
// v3_sql.SQLTx(tx),
// repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
// )
// return err
// }), nil
// }
+6
View File
@@ -101,6 +101,8 @@ var (
ProjectGrantRelationalProjection *handler.Handler
SessionRelationalProjection *handler.Handler
SettingsRelationalProjection *handler.Handler
// TODO(adlerhurst): Uncomment once users table is created and FK can be enforced
// IDPIntentRelationalProjection *handler.Handler
ProjectGrantFields *handler.FieldHandler
OrgDomainVerifiedFields *handler.FieldHandler
@@ -230,6 +232,8 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
AuthorizationRelationalProjection = newAuthorizationRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["authorizations_relational"]))
ProjectGrantRelationalProjection = newProjectGrantRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_relational"]))
SessionRelationalProjection = newSessionRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["session_relational"]))
// TODO(adlerhurst): Uncomment once users table is created and FK can be enforced
// IDPIntentRelationalProjection = newIDPIntentRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_intent_relational"]))
newProjectionsList()
newFieldsList()
@@ -427,5 +431,7 @@ func newProjectionsList() {
AuthorizationRelationalProjection,
ProjectGrantRelationalProjection,
SessionRelationalProjection,
// TODO(adlerhurst): Uncomment once users table is created and FK can be enforced
// IDPIntentRelationalProjection,
}
}