Livio Spring e57a9b57c8
feat(saml): allow setting nameid-format and alternative mapping for transient format (#7979)
# Which Problems Are Solved

ZITADEL currently always uses
`urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` in SAML requests,
relying on the IdP to respect that flag and always return a peristent
nameid in order to be able to map the external user with an existing
user (idp link) in ZITADEL.
In case the IdP however returns a
`urn:oasis:names:tc:SAML:2.0:nameid-format:transient` (transient)
nameid, the attribute will differ between each request and it will not
be possible to match existing users.

# How the Problems Are Solved

This PR adds the following two options on SAML IdP:
- **nameIDFormat**: allows to set the nameid-format used in the SAML
Request
- **transientMappingAttributeName**: allows to set an attribute name,
which will be used instead of the nameid itself in case the returned
nameid-format is transient

# Additional Changes

To reduce impact on current installations, the `idp_templates6_saml`
table is altered with the two added columns by a setup job. New
installations will automatically get the table with the two columns
directly.
All idp unit tests are updated to use `expectEventstore` instead of the
deprecated `eventstoreExpect`.

# Additional Context

Closes #7483
Closes #7743

---------

Co-authored-by: peintnermax <max@caos.ch>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
2024-05-23 05:04:07 +00:00

216 lines
5.2 KiB
Go

package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/zerrors"
)
type GenericOAuthProvider struct {
Name string
ClientID string
ClientSecret string
AuthorizationEndpoint string
TokenEndpoint string
UserEndpoint string
Scopes []string
IDAttribute string
IDPOptions idp.Options
}
type GenericOIDCProvider struct {
Name string
Issuer string
ClientID string
ClientSecret string
Scopes []string
IsIDTokenMapping bool
IDPOptions idp.Options
}
type JWTProvider struct {
Name string
Issuer string
JWTEndpoint string
KeyEndpoint string
HeaderName string
IDPOptions idp.Options
}
type AzureADProvider struct {
Name string
ClientID string
ClientSecret string
Scopes []string
Tenant string
EmailVerified bool
IDPOptions idp.Options
}
type GitHubProvider struct {
Name string
ClientID string
ClientSecret string
Scopes []string
IDPOptions idp.Options
}
type GitHubEnterpriseProvider struct {
Name string
ClientID string
ClientSecret string
AuthorizationEndpoint string
TokenEndpoint string
UserEndpoint string
Scopes []string
IDPOptions idp.Options
}
type GitLabProvider struct {
Name string
ClientID string
ClientSecret string
Scopes []string
IDPOptions idp.Options
}
type GitLabSelfHostedProvider struct {
Name string
Issuer string
ClientID string
ClientSecret string
Scopes []string
IDPOptions idp.Options
}
type GoogleProvider struct {
Name string
ClientID string
ClientSecret string
Scopes []string
IDPOptions idp.Options
}
type LDAPProvider struct {
Name string
Servers []string
StartTLS bool
BaseDN string
BindDN string
BindPassword string
UserBase string
UserObjectClasses []string
UserFilters []string
Timeout time.Duration
LDAPAttributes idp.LDAPAttributes
IDPOptions idp.Options
}
type SAMLProvider struct {
Name string
Metadata []byte
MetadataURL string
Binding string
WithSignedRequest bool
NameIDFormat *domain.SAMLNameIDFormat
TransientMappingAttributeName string
IDPOptions idp.Options
}
type AppleProvider struct {
Name string
ClientID string
TeamID string
KeyID string
PrivateKey []byte
Scopes []string
IDPOptions idp.Options
}
// ExistsIDPOnOrgOrInstance query first org level IDPs and then instance level IDPs, no check if the IDP is active
func ExistsIDPOnOrgOrInstance(ctx context.Context, filter preparation.FilterToQueryReducer, instanceID, orgID, id string) (exists bool, err error) {
writeModel := NewOrgIDPRemoveWriteModel(orgID, id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return false, err
}
if len(events) > 0 {
writeModel.AppendEvents(events...)
if err := writeModel.Reduce(); err != nil {
return false, err
}
return writeModel.State.Exists(), nil
}
instanceWriteModel := NewInstanceIDPRemoveWriteModel(instanceID, id)
events, err = filter(ctx, instanceWriteModel.Query())
if err != nil {
return false, err
}
if len(events) == 0 {
return false, nil
}
instanceWriteModel.AppendEvents(events...)
if err := instanceWriteModel.Reduce(); err != nil {
return false, err
}
return instanceWriteModel.State.Exists(), nil
}
// ExistsIDP query IDPs only with the ID, no check if the IDP is active
func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (exists bool, err error) {
writeModel := NewIDPTypeWriteModel(id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return false, err
}
if len(events) == 0 {
return false, nil
}
writeModel.AppendEvents(events...)
if err := writeModel.Reduce(); err != nil {
return false, err
}
return writeModel.State.Exists(), nil
}
func IDPProviderWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (_ *AllIDPWriteModel, err error) {
writeModel := NewIDPTypeWriteModel(id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-as02jin", "Errors.IDPConfig.NotExisting")
}
writeModel.AppendEvents(events...)
if err := writeModel.Reduce(); err != nil {
return nil, err
}
allWriteModel, err := NewAllIDPWriteModel(
writeModel.ResourceOwner,
writeModel.ResourceOwner == writeModel.InstanceID,
writeModel.ID,
writeModel.Type,
)
if err != nil {
return nil, err
}
events, err = filter(ctx, allWriteModel.Query())
if err != nil {
return nil, err
}
allWriteModel.AppendEvents(events...)
if err := allWriteModel.Reduce(); err != nil {
return nil, err
}
return allWriteModel, err
}