feat(saml): implementation of saml for ZITADEL v2 (#3618)

This commit is contained in:
Stefan Benz
2022-09-12 17:18:08 +01:00
committed by GitHub
parent 01a92ba5d9
commit 7a5f7f82cf
134 changed files with 5570 additions and 1293 deletions

View File

@@ -1,10 +1,11 @@
package command
import (
"net/http"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/http"
api_http "github.com/zitadel/zitadel/internal/api/http"
sd "github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
@@ -23,6 +24,8 @@ import (
)
type Commands struct {
httpClient *http.Client
eventstore *eventstore.Eventstore
static static.Storage
idGenerator id.Generator
@@ -40,14 +43,17 @@ type Commands struct {
applicationKeySize int
domainVerificationAlg crypto.EncryptionAlgorithm
domainVerificationGenerator crypto.Generator
domainVerificationValidator func(domain, token, verifier string, checkType http.CheckType) error
domainVerificationValidator func(domain, token, verifier string, checkType api_http.CheckType) error
multifactors domain.MultifactorConfigs
webauthnConfig *webauthn_helper.Config
keySize int
keyAlgorithm crypto.EncryptionAlgorithm
privateKeyLifetime time.Duration
publicKeyLifetime time.Duration
multifactors domain.MultifactorConfigs
webauthnConfig *webauthn_helper.Config
keySize int
keyAlgorithm crypto.EncryptionAlgorithm
certificateAlgorithm crypto.EncryptionAlgorithm
certKeySize int
privateKeyLifetime time.Duration
publicKeyLifetime time.Duration
certificateLifetime time.Duration
}
func StartCommands(es *eventstore.Eventstore,
@@ -64,7 +70,9 @@ func StartCommands(es *eventstore.Eventstore,
smsEncryption,
userEncryption,
domainVerificationEncryption,
oidcEncryption crypto.EncryptionAlgorithm,
oidcEncryption,
samlEncryption crypto.EncryptionAlgorithm,
httpClient *http.Client,
) (repo *Commands, err error) {
if externalDomain == "" {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
@@ -78,15 +86,19 @@ func StartCommands(es *eventstore.Eventstore,
externalSecure: externalSecure,
externalPort: externalPort,
keySize: defaults.KeyConfig.Size,
certKeySize: defaults.KeyConfig.CertificateSize,
privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
certificateLifetime: defaults.KeyConfig.CertificateLifetime,
idpConfigEncryption: idpConfigEncryption,
smtpEncryption: smtpEncryption,
smsEncryption: smsEncryption,
userEncryption: userEncryption,
domainVerificationAlg: domainVerificationEncryption,
keyAlgorithm: oidcEncryption,
certificateAlgorithm: samlEncryption,
webauthnConfig: webAuthN,
httpClient: httpClient,
}
instance_repo.RegisterEventMappers(repo.eventstore)
@@ -109,7 +121,7 @@ func StartCommands(es *eventstore.Eventstore,
}
repo.domainVerificationGenerator = crypto.NewEncryptionGenerator(defaults.DomainVerification.VerificationGenerator, repo.domainVerificationAlg)
repo.domainVerificationValidator = http.ValidateDomain
repo.domainVerificationValidator = api_http.ValidateDomain
return repo, nil
}

View File

@@ -2,6 +2,10 @@ package command
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"math/big"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
@@ -34,3 +38,138 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string)
privateKeyExp, publicKeyExp))
return err
}
func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm string) error {
now := time.Now().UTC()
after := now.Add(c.certificateLifetime)
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
if err != nil {
return err
}
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCACertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, &crypto.CertificateInformations{
SerialNumber: randInt,
Organisation: []string{"ZITADEL"},
CommonName: "ZITADEL SAML CA",
NotBefore: now,
NotAfter: after,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
})
if err != nil {
return err
}
keyID, err := c.idGenerator.Next()
if err != nil {
return err
}
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
_, err = c.eventstore.Push(ctx,
keypair.NewAddedEvent(
ctx,
keyAgg,
domain.KeyUsageSAMLCA,
algorithm,
privateCrypto, publicCrypto,
after, after,
),
keypair.NewAddedCertificateEvent(
ctx,
keyAgg,
certificateCrypto,
after,
),
)
return err
}
func (c *Commands) GenerateSAMLResponseCertificate(ctx context.Context, algorithm string, caPrivateKey *rsa.PrivateKey, caCertificate []byte) error {
now := time.Now().UTC()
after := now.Add(c.certificateLifetime)
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
if err != nil {
return err
}
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, caPrivateKey, caCertificate, &crypto.CertificateInformations{
SerialNumber: randInt,
Organisation: []string{"ZITADEL"},
CommonName: "ZITADEL SAML response",
NotBefore: now,
NotAfter: after,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
})
if err != nil {
return err
}
keyID, err := c.idGenerator.Next()
if err != nil {
return err
}
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
_, err = c.eventstore.Push(ctx,
keypair.NewAddedEvent(
ctx,
keyAgg,
domain.KeyUsageSAMLResponseSinging,
algorithm,
privateCrypto, publicCrypto,
after, after,
),
keypair.NewAddedCertificateEvent(
ctx,
keyAgg,
certificateCrypto,
after,
),
)
return err
}
func (c *Commands) GenerateSAMLMetadataCertificate(ctx context.Context, algorithm string, caPrivateKey *rsa.PrivateKey, caCertificate []byte) error {
now := time.Now().UTC()
after := now.Add(c.certificateLifetime)
randInt, err := rand.Int(rand.Reader, big.NewInt(1000))
if err != nil {
return err
}
privateCrypto, publicCrypto, certificateCrypto, err := crypto.GenerateEncryptedKeyPairWithCertificate(c.certKeySize, c.keyAlgorithm, c.certificateAlgorithm, caPrivateKey, caCertificate, &crypto.CertificateInformations{
SerialNumber: randInt,
Organisation: []string{"ZITADEL"},
CommonName: "ZITADEL SAML metadata",
NotBefore: now,
NotAfter: after,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
})
if err != nil {
return err
}
keyID, err := c.idGenerator.Next()
if err != nil {
return err
}
keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel)
_, err = c.eventstore.Push(ctx,
keypair.NewAddedEvent(
ctx,
keyAgg,
domain.KeyUsageSAMLMetadataSigning,
algorithm,
privateCrypto, publicCrypto,
after, after),
keypair.NewAddedCertificateEvent(
ctx,
keyAgg,
certificateCrypto,
after,
),
)
return err
}

View File

@@ -9,10 +9,11 @@ import (
type KeyPairWriteModel struct {
eventstore.WriteModel
Usage domain.KeyUsage
Algorithm string
PrivateKey *domain.Key
PublicKey *domain.Key
Usage domain.KeyUsage
Algorithm string
PrivateKey *domain.Key
PublicKey *domain.Key
Certificate *domain.Key
}
func NewKeyPairWriteModel(aggregateID, resourceOwner string) *KeyPairWriteModel {
@@ -42,6 +43,11 @@ func (wm *KeyPairWriteModel) Reduce() error {
Key: e.PublicKey.Key,
Expiry: e.PublicKey.Expiry,
}
case *keypair.AddedCertificateEvent:
wm.Certificate = &domain.Key{
Key: e.Certificate.Key,
Expiry: e.Certificate.Expiry,
}
}
}
return wm.WriteModel.Reduce()
@@ -53,11 +59,10 @@ func (wm *KeyPairWriteModel) Query() *eventstore.SearchQueryBuilder {
AddQuery().
AggregateTypes(keypair.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(keypair.AddedEventType).
EventTypes(keypair.AddedEventType, keypair.AddedCertificateEventType).
Builder()
}
func KeyPairAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return eventstore.AggregateFromWriteModel(wm, keypair.AggregateType, keypair.AggregateVersion)
}

View File

@@ -5,6 +5,7 @@ import (
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
@@ -276,9 +277,20 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
if existingProject.State == domain.ProjectStateUnspecified || existingProject.State == domain.ProjectStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
}
samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, projectID, resourceOwner)
if err != nil {
return nil, err
}
uniqueConstraints := make([]*eventstore.EventUniqueConstraint, len(samlEntityIDsAgg.EntityIDs))
for i, entityID := range samlEntityIDsAgg.EntityIDs {
uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID)
}
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
events := []eventstore.Command{
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name),
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints),
}
for _, grantID := range cascadingUserGrantIDs {
@@ -309,3 +321,12 @@ func (c *Commands) getProjectWriteModelByID(ctx context.Context, projectID, reso
}
return projectWriteModel, nil
}
func (c *Commands) getSAMLEntityIdsWriteModelByProjectID(ctx context.Context, projectID, resourceOwner string) (*SAMLEntityIDsWriteModel, error) {
samlEntityIDsAgg := NewSAMLEntityIDsWriteModel(projectID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, samlEntityIDsAgg)
if err != nil {
return nil, err
}
return samlEntityIDsAgg, nil
}

View File

@@ -118,7 +118,13 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso
}
projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name))
entityID := ""
samlWriteModel, err := c.getSAMLAppWriteModel(ctx, projectID, appID, resourceOwner)
if err == nil && samlWriteModel.State != domain.AppStateUnspecified && samlWriteModel.State != domain.AppStateRemoved && samlWriteModel.saml {
entityID = samlWriteModel.EntityID
}
pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name, entityID))
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,146 @@
package command
import (
"context"
"github.com/zitadel/saml/pkg/provider/xml"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
)
func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.SAMLApp, resourceOwner string) (_ *domain.SAMLApp, err error) {
if application == nil || application.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid")
}
_, err = c.getProjectByID(ctx, application.AggregateID, resourceOwner)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-3p9ss", "Errors.Project.NotFound")
}
addedApplication := NewSAMLApplicationWriteModel(application.AggregateID, resourceOwner)
projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel)
events, err := c.addSAMLApplication(ctx, projectAgg, application)
if err != nil {
return nil, err
}
addedApplication.AppID = application.AppID
pushedEvents, err := c.eventstore.Push(ctx, events...)
if err != nil {
return nil, err
}
err = AppendAndReduce(addedApplication, pushedEvents...)
if err != nil {
return nil, err
}
result := samlWriteModelToSAMLConfig(addedApplication)
return result, nil
}
func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstore.Aggregate, samlApp *domain.SAMLApp) (events []eventstore.Command, err error) {
if samlApp.AppName == "" || !samlApp.IsValid() {
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-1n9df", "Errors.Project.App.Invalid")
}
if samlApp.Metadata == nil && samlApp.MetadataURL == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "SAML-podix9", "Errors.Project.App.SAMLMetadataMissing")
}
if samlApp.MetadataURL != "" {
data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
if err != nil {
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-wmqlo1", "Errors.Project.App.SAMLMetadataMissing")
}
samlApp.Metadata = data
}
entity, err := xml.ParseMetadataXmlIntoStruct(samlApp.Metadata)
if err != nil {
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-bquso", "Errors.Project.App.SAMLMetadataFormat")
}
samlApp.AppID, err = c.idGenerator.Next()
if err != nil {
return nil, err
}
return []eventstore.Command{
project.NewApplicationAddedEvent(ctx, projectAgg, samlApp.AppID, samlApp.AppName),
project.NewSAMLConfigAddedEvent(ctx,
projectAgg,
samlApp.AppID,
string(entity.EntityID),
samlApp.Metadata,
samlApp.MetadataURL,
),
}, nil
}
func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) {
if !samlApp.IsValid() || samlApp.AppID == "" || samlApp.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5n9fs", "Errors.Project.App.SAMLConfigInvalid")
}
existingSAML, err := c.getSAMLAppWriteModel(ctx, samlApp.AggregateID, samlApp.AppID, resourceOwner)
if err != nil {
return nil, err
}
if existingSAML.State == domain.AppStateUnspecified || existingSAML.State == domain.AppStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2n8uU", "Errors.Project.App.NotExisting")
}
if !existingSAML.IsSAML() {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-GBr35", "Errors.Project.App.IsNotSAML")
}
projectAgg := ProjectAggregateFromWriteModel(&existingSAML.WriteModel)
if samlApp.MetadataURL != "" {
data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL)
if err != nil {
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-J3kg3", "Errors.Project.App.SAMLMetadataMissing")
}
samlApp.Metadata = data
}
entity, err := xml.ParseMetadataXmlIntoStruct(samlApp.Metadata)
if err != nil {
return nil, caos_errs.ThrowInvalidArgument(err, "SAML-3fk2b", "Errors.Project.App.SAMLMetadataFormat")
}
changedEvent, hasChanged, err := existingSAML.NewChangedEvent(
ctx,
projectAgg,
samlApp.AppID,
string(entity.EntityID),
samlApp.Metadata,
samlApp.MetadataURL)
if err != nil {
return nil, err
}
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1m88i", "Errors.NoChangesFound")
}
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingSAML, pushedEvents...)
if err != nil {
return nil, err
}
return samlWriteModelToSAMLConfig(existingSAML), nil
}
func (c *Commands) getSAMLAppWriteModel(ctx context.Context, projectID, appID, resourceOwner string) (*SAMLApplicationWriteModel, error) {
appWriteModel := NewSAMLApplicationWriteModelWithAppID(projectID, appID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, appWriteModel)
if err != nil {
return nil, err
}
return appWriteModel, nil
}

View File

@@ -0,0 +1,268 @@
package command
import (
"context"
"reflect"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/project"
)
type SAMLApplicationWriteModel struct {
eventstore.WriteModel
AppID string
AppName string
EntityID string
Metadata []byte
MetadataURL string
State domain.AppState
saml bool
}
func NewSAMLApplicationWriteModelWithAppID(projectID, appID, resourceOwner string) *SAMLApplicationWriteModel {
return &SAMLApplicationWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: projectID,
ResourceOwner: resourceOwner,
},
AppID: appID,
}
}
func NewSAMLApplicationWriteModel(projectID, resourceOwner string) *SAMLApplicationWriteModel {
return &SAMLApplicationWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: projectID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *SAMLApplicationWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *project.ApplicationAddedEvent:
if e.AppID != wm.AppID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.ApplicationChangedEvent:
if e.AppID != wm.AppID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.ApplicationDeactivatedEvent:
if e.AppID != wm.AppID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.ApplicationReactivatedEvent:
if e.AppID != wm.AppID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.ApplicationRemovedEvent:
if e.AppID != wm.AppID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.SAMLConfigAddedEvent:
if e.AppID != wm.AppID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.SAMLConfigChangedEvent:
if e.AppID != wm.AppID {
continue
}
wm.WriteModel.AppendEvents(e)
case *project.ProjectRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
func (wm *SAMLApplicationWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *project.ApplicationAddedEvent:
wm.AppName = e.Name
wm.State = domain.AppStateActive
case *project.ApplicationChangedEvent:
wm.AppName = e.Name
case *project.ApplicationDeactivatedEvent:
if wm.State == domain.AppStateRemoved {
continue
}
wm.State = domain.AppStateInactive
case *project.ApplicationReactivatedEvent:
if wm.State == domain.AppStateRemoved {
continue
}
wm.State = domain.AppStateActive
case *project.ApplicationRemovedEvent:
wm.State = domain.AppStateRemoved
case *project.SAMLConfigAddedEvent:
wm.appendAddSAMLEvent(e)
case *project.SAMLConfigChangedEvent:
wm.appendChangeSAMLEvent(e)
case *project.ProjectRemovedEvent:
wm.State = domain.AppStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *SAMLApplicationWriteModel) appendAddSAMLEvent(e *project.SAMLConfigAddedEvent) {
wm.saml = true
wm.Metadata = e.Metadata
wm.MetadataURL = e.MetadataURL
wm.EntityID = e.EntityID
}
func (wm *SAMLApplicationWriteModel) appendChangeSAMLEvent(e *project.SAMLConfigChangedEvent) {
wm.saml = true
if e.Metadata != nil {
wm.Metadata = e.Metadata
}
if e.MetadataURL != nil {
wm.MetadataURL = *e.MetadataURL
}
if e.EntityID != "" {
wm.EntityID = e.EntityID
}
}
func (wm *SAMLApplicationWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
project.ApplicationAddedType,
project.ApplicationChangedType,
project.ApplicationDeactivatedType,
project.ApplicationReactivatedType,
project.ApplicationRemovedType,
project.SAMLConfigAddedType,
project.SAMLConfigChangedType,
project.ProjectRemovedType).
Builder()
}
func (wm *SAMLApplicationWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
appID string,
entityID string,
metadata []byte,
metadataURL string,
) (*project.SAMLConfigChangedEvent, bool, error) {
changes := make([]project.SAMLConfigChanges, 0)
var err error
if !reflect.DeepEqual(wm.Metadata, metadata) {
changes = append(changes, project.ChangeMetadata(metadata))
}
if wm.MetadataURL != metadataURL {
changes = append(changes, project.ChangeMetadataURL(metadataURL))
}
if wm.EntityID != entityID {
changes = append(changes, project.ChangeEntityID(entityID))
}
if len(changes) == 0 {
return nil, false, nil
}
changeEvent, err := project.NewSAMLConfigChangedEvent(ctx, aggregate, appID, wm.EntityID, changes)
if err != nil {
return nil, false, err
}
return changeEvent, true, nil
}
func (wm *SAMLApplicationWriteModel) IsSAML() bool {
return wm.saml
}
type AppIDToEntityID struct {
AppID string
EntityID string
}
type SAMLEntityIDsWriteModel struct {
eventstore.WriteModel
EntityIDs []*AppIDToEntityID
}
func NewSAMLEntityIDsWriteModel(projectID, resourceOwner string) *SAMLEntityIDsWriteModel {
return &SAMLEntityIDsWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: projectID,
ResourceOwner: resourceOwner,
},
EntityIDs: []*AppIDToEntityID{},
}
}
func (wm *SAMLEntityIDsWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
project.ApplicationRemovedType,
project.SAMLConfigAddedType,
project.SAMLConfigChangedType).
Builder()
}
func (wm *SAMLEntityIDsWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *project.ApplicationRemovedEvent:
wm.WriteModel.AppendEvents(e)
case *project.SAMLConfigAddedEvent:
wm.WriteModel.AppendEvents(e)
case *project.SAMLConfigChangedEvent:
if e.EntityID != "" {
wm.WriteModel.AppendEvents(e)
}
}
}
}
func (wm *SAMLEntityIDsWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *project.ApplicationRemovedEvent:
removeAppIDFromEntityIDs(wm.EntityIDs, e.AppID)
case *project.SAMLConfigAddedEvent:
wm.EntityIDs = append(wm.EntityIDs, &AppIDToEntityID{AppID: e.AppID, EntityID: e.EntityID})
case *project.SAMLConfigChangedEvent:
for i := range wm.EntityIDs {
item := wm.EntityIDs[i]
if e.AppID == item.AppID && e.EntityID != "" {
item.EntityID = e.EntityID
}
}
}
}
return wm.WriteModel.Reduce()
}
func removeAppIDFromEntityIDs(items []*AppIDToEntityID, appID string) []*AppIDToEntityID {
for i := len(items) - 1; i >= 0; i-- {
if items[i].AppID == appID {
items[i] = items[len(items)-1]
items[len(items)-1] = nil
items = items[:len(items)-1]
}
}
return items
}

View File

@@ -0,0 +1,776 @@
package command
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/project"
)
var testMetadata = []byte(`<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
validUntil="2022-08-26T14:08:16Z"
cacheDuration="PT604800S"
entityID="https://test.com/saml/metadata">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://test.com/saml/acs"
index="1" />
</md:SPSSODescriptor>
</md:EntityDescriptor>
`)
var testMetadataChangedEntityID = []byte(`<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
validUntil="2022-08-26T14:08:16Z"
cacheDuration="PT604800S"
entityID="https://test2.com/saml/metadata">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://test.com/saml/acs"
index="1" />
</md:SPSSODescriptor>
</md:EntityDescriptor>
`)
func TestCommandSide_AddSAMLApplication(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
httpClient *http.Client
}
type args struct {
ctx context.Context
samlApp *domain.SAMLApp
resourceOwner string
}
type res struct {
want *domain.SAMLApp
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "no aggregate id, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{},
resourceOwner: "org1",
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
{
name: "project not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
AppName: "app",
},
resourceOwner: "org1",
},
res: res{
err: errors.IsPreconditionFailed,
},
},
{
name: "invalid app, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
AppName: "",
},
resourceOwner: "org1",
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
{
name: "create saml app, metadata not parsable",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: []byte("test metadata"),
MetadataURL: "",
},
resourceOwner: "org1",
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
{
name: "create saml app, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
testMetadata,
"",
),
),
},
uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")),
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1"),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
},
resourceOwner: "org1",
},
res: res{
want: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
State: domain.AppStateActive,
},
},
},
{
name: "create saml app metadataURL, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
testMetadata,
"http://localhost:8080/saml/metadata",
),
),
},
uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")),
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1"),
httpClient: newTestClient(200, testMetadata),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
},
resourceOwner: "org1",
},
res: res{
want: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "http://localhost:8080/saml/metadata",
State: domain.AppStateActive,
},
},
},
{
name: "create saml app metadataURL, http error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t),
httpClient: newTestClient(http.StatusNotFound, nil),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
EntityID: "https://test.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
},
resourceOwner: "org1",
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
httpClient: tt.fields.httpClient,
}
got, err := r.AddSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_ChangeSAMLApplication(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
httpClient *http.Client
}
type args struct {
ctx context.Context
samlApp *domain.SAMLApp
resourceOwner string
}
type res struct {
want *domain.SAMLApp
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "invalid app, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
},
resourceOwner: "org1",
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
{
name: "missing appid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "",
Metadata: []byte("just not empty"),
},
resourceOwner: "org1",
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
{
name: "missing aggregateid, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "",
},
AppID: "appid",
Metadata: []byte("just not empty"),
},
resourceOwner: "org1",
},
res: res{
err: errors.IsErrorInvalidArgument,
},
},
{
name: "app not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
Metadata: []byte("just not empty"),
},
resourceOwner: "org1",
},
res: res{
err: errors.IsNotFound,
},
},
{
name: "no changes, precondition error, metadataURL",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
testMetadata,
"http://localhost:8080/saml/metadata",
),
),
),
),
httpClient: newTestClient(http.StatusOK, testMetadata),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppName: "app",
AppID: "app1",
EntityID: "https://test.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
},
resourceOwner: "org1",
},
res: res{
err: errors.IsPreconditionFailed,
},
},
{
name: "no changes, precondition error, metadata",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
testMetadata,
"",
),
),
),
),
httpClient: nil,
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppName: "app",
AppID: "app1",
EntityID: "https://test.com/saml/metadata",
Metadata: testMetadata,
MetadataURL: "",
},
resourceOwner: "org1",
},
res: res{
err: errors.IsPreconditionFailed,
},
},
{
name: "change saml app, ok, metadataURL",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
testMetadata,
"http://localhost:8080/saml/metadata",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newSAMLAppChangedEventMetadataURL(context.Background(),
"app1",
"project1",
"org1",
"https://test.com/saml/metadata",
"https://test2.com/saml/metadata",
testMetadataChangedEntityID,
),
),
},
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
),
),
httpClient: newTestClient(http.StatusOK, testMetadataChangedEntityID),
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: nil,
MetadataURL: "http://localhost:8080/saml/metadata",
},
resourceOwner: "org1",
},
res: res{
want: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "http://localhost:8080/saml/metadata",
State: domain.AppStateActive,
},
},
},
{
name: "change saml app, ok, metadata",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
testMetadata,
"",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newSAMLAppChangedEventMetadata(context.Background(),
"app1",
"project1",
"org1",
"https://test.com/saml/metadata",
"https://test2.com/saml/metadata",
testMetadataChangedEntityID,
),
),
},
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
uniqueConstraintsFromEventConstraint(project.NewAddSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
),
),
httpClient: nil,
},
args: args{
ctx: context.Background(),
samlApp: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "",
},
resourceOwner: "org1",
},
res: res{
want: &domain.SAMLApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
ResourceOwner: "org1",
},
AppID: "app1",
AppName: "app",
EntityID: "https://test2.com/saml/metadata",
Metadata: testMetadataChangedEntityID,
MetadataURL: "",
State: domain.AppStateActive,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
httpClient: tt.fields.httpClient,
}
got, err := r.ChangeSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func newSAMLAppChangedEventMetadata(ctx context.Context, appID, projectID, resourceOwner, oldEntityID, entityID string, metadata []byte) *project.SAMLConfigChangedEvent {
changes := []project.SAMLConfigChanges{
project.ChangeEntityID(entityID),
project.ChangeMetadata(metadata),
}
event, _ := project.NewSAMLConfigChangedEvent(ctx,
&project.NewAggregate(projectID, resourceOwner).Aggregate,
appID,
oldEntityID,
changes,
)
return event
}
func newSAMLAppChangedEventMetadataURL(ctx context.Context, appID, projectID, resourceOwner, oldEntityID, entityID string, metadata []byte) *project.SAMLConfigChangedEvent {
changes := []project.SAMLConfigChanges{
project.ChangeEntityID(entityID),
project.ChangeMetadata(metadata),
}
event, _ := project.NewSAMLConfigChangedEvent(ctx,
&project.NewAggregate(projectID, resourceOwner).Aggregate,
appID,
oldEntityID,
changes,
)
return event
}
type roundTripperFunc func(*http.Request) *http.Response
// RoundTrip implements the http.RoundTripper interface.
func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req), nil
}
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
func newTestClient(httpStatus int, metadata []byte) *http.Client {
fn := roundTripperFunc(func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: httpStatus,
Body: ioutil.NopCloser(bytes.NewBuffer(metadata)),
Header: make(http.Header), //must be non-nil value
}
})
return &http.Client{
Transport: fn,
}
}

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -580,6 +581,58 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
err: caos_errs.IsNotFound,
},
},
{
name: "app remove, entityID, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
)),
),
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
)),
eventFromEventPusher(project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
"",
)),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
"https://test.com/saml/metadata",
)),
}, /**/
uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")),
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
appID: "app1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "app remove, ok",
fields: fields{
@@ -592,12 +645,15 @@ func TestCommandSide_RemoveApplication(t *testing.T) {
"app",
)),
),
// app is not saml, or no saml config available
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
"",
)),
},
uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")),

View File

@@ -57,6 +57,18 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O
}
}
func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.SAMLApp {
return &domain.SAMLApp{
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),
AppID: writeModel.AppID,
AppName: writeModel.AppName,
State: writeModel.State,
Metadata: writeModel.Metadata,
MetadataURL: writeModel.MetadataURL,
EntityID: writeModel.EntityID,
}
}
func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIApp {
return &domain.APIApp{
ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel),

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -50,6 +51,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) {
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectname1",
nil,
),
),
),
@@ -253,6 +255,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) {
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectname1",
nil,
),
),
),
@@ -503,6 +506,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) {
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectname1",
nil,
),
),
),

View File

@@ -269,7 +269,8 @@ func TestCommandSide_ChangeProject(t *testing.T) {
eventFromEventPusher(
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project"),
"project",
nil),
),
),
),
@@ -542,7 +543,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) {
eventFromEventPusher(
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project"),
"project",
nil),
),
),
),
@@ -721,7 +723,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) {
eventFromEventPusher(
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project"),
"project",
nil),
),
),
),
@@ -900,7 +903,8 @@ func TestCommandSide_RemoveProject(t *testing.T) {
eventFromEventPusher(
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project"),
"project",
nil),
),
),
),
@@ -915,7 +919,7 @@ func TestCommandSide_RemoveProject(t *testing.T) {
},
},
{
name: "project remove, ok",
name: "project remove, without entityConstraints, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
@@ -927,12 +931,15 @@ func TestCommandSide_RemoveProject(t *testing.T) {
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
),
),
// no saml application events
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project"),
"project",
nil),
),
},
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
@@ -950,6 +957,150 @@ func TestCommandSide_RemoveProject(t *testing.T) {
},
},
},
{
name: "project remove, with entityConstraints, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
),
),
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
)),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test.com/saml/metadata",
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
"http://localhost:8080/saml/metadata",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
[]*eventstore.EventUniqueConstraint{
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata"),
}),
),
},
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata")),
),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "project remove, with multiple entityConstraints, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy),
),
),
expectFilter(
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
)),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"https://test1.com/saml/metadata",
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
"",
),
),
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app2",
"app",
)),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app2",
"https://test2.com/saml/metadata",
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
"",
),
),
eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app3",
"app",
)),
eventFromEventPusher(
project.NewSAMLConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app3",
"https://test3.com/saml/metadata",
[]byte("<?xml version=\"1.0\"?>\n<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n validUntil=\"2022-08-26T14:08:16Z\"\n cacheDuration=\"PT604800S\"\n entityID=\"https://test.com/saml/metadata\">\n <md:SPSSODescriptor AuthnRequestsSigned=\"false\" WantAssertionsSigned=\"false\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>\n <md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n Location=\"https://test.com/saml/acs\"\n index=\"1\" />\n \n </md:SPSSODescriptor>\n</md:EntityDescriptor>"),
"",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project",
[]*eventstore.EventUniqueConstraint{
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata"),
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata"),
project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata"),
}),
),
},
uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")),
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata")),
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata")),
uniqueConstraintsFromEventConstraint(project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata")),
),
),
},
args: args{
ctx: context.Background(),
projectID: "project1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
@@ -92,6 +93,10 @@ func (rm *UniqueConstraintReadModel) Reduce() error {
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
case *project.ApplicationChangedEvent:
rm.changeUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID))
case *project.SAMLConfigAddedEvent:
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewAddSAMLConfigEntityIDUniqueConstraint(e.EntityID))
case *project.SAMLConfigChangedEvent:
rm.addUniqueConstraint(e.Aggregate().ID, e.AppID, project.NewRemoveSAMLConfigEntityIDUniqueConstraint(e.EntityID))
case *project.ApplicationRemovedEvent:
rm.removeUniqueConstraint(e.Aggregate().ID, e.AppID, project.UniqueAppNameType)
case *project.GrantAddedEvent:

View File

@@ -133,6 +133,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) {
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectname1",
nil,
),
),
),
@@ -819,6 +820,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) {
project.NewProjectRemovedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"projectname1",
nil,
),
),
),