feat: jwt as idp (#2363)

* feat: jwt idp

* feat: command side

* feat: add tests

* fill idp views with jwt idps and return apis

* add jwtEndpoint to jwt idp

* begin jwt request handling

* merge

* handle jwt idp

* cleanup

* fixes

* autoregister

* get token from specific header name

* error handling

* fix texts

* handle renderExternalNotFoundOption

Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
This commit is contained in:
Livio Amstutz
2021-09-14 15:15:01 +02:00
committed by GitHub
parent 4e1d42259c
commit b6b5b1b782
54 changed files with 2575 additions and 71 deletions

View File

@@ -153,6 +153,17 @@ func writeModelToIDPOIDCConfig(wm *OIDCConfigWriteModel) *domain.OIDCIDPConfig {
}
}
func writeModelToIDPJWTConfig(wm *JWTConfigWriteModel) *domain.JWTIDPConfig {
return &domain.JWTIDPConfig{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
IDPConfigID: wm.IDPConfigID,
JWTEndpoint: wm.JWTEndpoint,
Issuer: wm.Issuer,
KeysEndpoint: wm.KeysEndpoint,
HeaderName: wm.HeaderName,
}
}
func writeModelToIDPProvider(wm *IdentityProviderWriteModel) *domain.IDPProvider {
return &domain.IDPProvider{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),

View File

@@ -2,6 +2,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@@ -13,7 +14,7 @@ import (
)
func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPConfig) (*domain.IDPConfig, error) {
if config.OIDCConfig == nil {
if config.OIDCConfig == nil && config.JWTConfig == nil {
return nil, errors.ThrowInvalidArgument(nil, "IAM-eUpQU", "Errors.idp.config.notset")
}
@@ -23,11 +24,6 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo
}
addedConfig := NewIAMIDPConfigWriteModel(idpConfigID)
clientSecret, err := crypto.Encrypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto)
if err != nil {
return nil, err
}
iamAgg := IAMAggregateFromWriteModel(&addedConfig.WriteModel)
events := []eventstore.EventPusher{
iam_repo.NewIDPConfigAddedEvent(
@@ -39,7 +35,14 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo
config.StylingType,
config.AutoRegister,
),
iam_repo.NewIDPOIDCConfigAddedEvent(
}
if config.OIDCConfig != nil {
clientSecret, err := crypto.Encrypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto)
if err != nil {
return nil, err
}
events = append(events, iam_repo.NewIDPOIDCConfigAddedEvent(
ctx,
iamAgg,
config.OIDCConfig.ClientID,
@@ -51,9 +54,18 @@ func (c *Commands) AddDefaultIDPConfig(ctx context.Context, config *domain.IDPCo
config.OIDCConfig.IDPDisplayNameMapping,
config.OIDCConfig.UsernameMapping,
config.OIDCConfig.Scopes...,
),
))
} else if config.JWTConfig != nil {
events = append(events, iam_repo.NewIDPJWTConfigAddedEvent(
ctx,
iamAgg,
idpConfigID,
config.JWTConfig.JWTEndpoint,
config.JWTConfig.Issuer,
config.JWTConfig.KeysEndpoint,
config.JWTConfig.HeaderName,
))
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err

View File

@@ -129,6 +129,65 @@ func TestCommandSide_AddDefaultIDPConfig(t *testing.T) {
},
},
},
{
name: "idp config jwt add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectPush(
[]*repository.Event{
eventFromEventPusher(
iam.NewIDPConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"name1",
domain.IDPConfigTypeOIDC,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
iam.NewIDPJWTConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
},
uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name1", "IAM")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "config1"),
},
args: args{
ctx: context.Background(),
config: &domain.IDPConfig{
Name: "name1",
StylingType: domain.IDPConfigStylingTypeGoogle,
JWTConfig: &domain.JWTIDPConfig{
JWTEndpoint: "jwt-endpoint",
Issuer: "issuer",
KeysEndpoint: "keys-endpoint",
HeaderName: "auth",
},
},
},
res: res{
want: &domain.IDPConfig{
ObjectRoot: models.ObjectRoot{
AggregateID: "IAM",
ResourceOwner: "IAM",
},
IDPConfigID: "config1",
Name: "name1",
StylingType: domain.IDPConfigStylingTypeGoogle,
State: domain.IDPConfigStateActive,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -0,0 +1,49 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
)
func (c *Commands) ChangeDefaultIDPJWTConfig(ctx context.Context, config *domain.JWTIDPConfig) (*domain.JWTIDPConfig, error) {
if config.IDPConfigID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-m9322", "Errors.IDMissing")
}
existingConfig := NewIAMIDPJWTConfigWriteModel(config.IDPConfigID)
err := c.eventstore.FilterToQueryReducer(ctx, existingConfig)
if err != nil {
return nil, err
}
if existingConfig.State == domain.IDPConfigStateRemoved || existingConfig.State == domain.IDPConfigStateUnspecified {
return nil, caos_errs.ThrowNotFound(nil, "IAM-2m00d", "Errors.IAM.IDPConfig.AlreadyExists")
}
iamAgg := IAMAggregateFromWriteModel(&existingConfig.WriteModel)
changedEvent, hasChanged, err := existingConfig.NewChangedEvent(
ctx,
iamAgg,
config.IDPConfigID,
config.JWTEndpoint,
config.Issuer,
config.KeysEndpoint,
config.HeaderName)
if err != nil {
return nil, err
}
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-3n9gg", "Errors.IAM.IDPConfig.NotChanged")
}
pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingConfig, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToIDPJWTConfig(&existingConfig.JWTConfigWriteModel), nil
}

View File

@@ -0,0 +1,116 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/repository/iam"
"github.com/caos/zitadel/internal/repository/idpconfig"
)
type IAMIDPJWTConfigWriteModel struct {
JWTConfigWriteModel
}
func NewIAMIDPJWTConfigWriteModel(idpConfigID string) *IAMIDPJWTConfigWriteModel {
return &IAMIDPJWTConfigWriteModel{
JWTConfigWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: domain.IAMID,
ResourceOwner: domain.IAMID,
},
IDPConfigID: idpConfigID,
},
}
}
func (wm *IAMIDPJWTConfigWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *iam.IDPJWTConfigAddedEvent:
if wm.IDPConfigID != e.IDPConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigAddedEvent)
case *iam.IDPJWTConfigChangedEvent:
if wm.IDPConfigID != e.IDPConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigChangedEvent)
case *iam.IDPConfigReactivatedEvent:
if wm.IDPConfigID != e.ConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigReactivatedEvent)
case *iam.IDPConfigDeactivatedEvent:
if wm.IDPConfigID != e.ConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigDeactivatedEvent)
case *iam.IDPConfigRemovedEvent:
if wm.IDPConfigID != e.ConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigRemovedEvent)
default:
wm.JWTConfigWriteModel.AppendEvents(e)
}
}
}
func (wm *IAMIDPJWTConfigWriteModel) Reduce() error {
if err := wm.JWTConfigWriteModel.Reduce(); err != nil {
return err
}
return wm.WriteModel.Reduce()
}
func (wm *IAMIDPJWTConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(iam.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
iam.IDPJWTConfigAddedEventType,
iam.IDPJWTConfigChangedEventType,
iam.IDPConfigReactivatedEventType,
iam.IDPConfigDeactivatedEventType,
iam.IDPConfigRemovedEventType).
Builder()
}
func (wm *IAMIDPJWTConfigWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
idpConfigID,
jwtEndpoint,
issuer,
keysEndpoint,
headerName string,
) (*iam.IDPJWTConfigChangedEvent, bool, error) {
changes := make([]idpconfig.JWTConfigChanges, 0)
if wm.JWTEndpoint != jwtEndpoint {
changes = append(changes, idpconfig.ChangeJWTEndpoint(jwtEndpoint))
}
if wm.Issuer != issuer {
changes = append(changes, idpconfig.ChangeJWTIssuer(issuer))
}
if wm.KeysEndpoint != keysEndpoint {
changes = append(changes, idpconfig.ChangeKeysEndpoint(keysEndpoint))
}
if wm.HeaderName != headerName {
changes = append(changes, idpconfig.ChangeHeaderName(headerName))
}
if len(changes) == 0 {
return nil, false, nil
}
changeEvent, err := iam.NewIDPJWTConfigChangedEvent(ctx, aggregate, idpConfigID, changes)
if err != nil {
return nil, false, err
}
return changeEvent, true, nil
}

View File

@@ -0,0 +1,264 @@
package command
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/repository/iam"
"github.com/caos/zitadel/internal/repository/idpconfig"
)
func TestCommandSide_ChangeDefaultIDPJWTConfig(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type (
args struct {
ctx context.Context
config *domain.JWTIDPConfig
}
)
type res struct {
want *domain.JWTIDPConfig
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "invalid config, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "idp config not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
},
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "idp config removed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
iam.NewIDPConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"name1",
domain.IDPConfigTypeJWT,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
iam.NewIDPJWTConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
eventFromEventPusher(
iam.NewIDPConfigRemovedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"name",
),
),
),
),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
},
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
iam.NewIDPConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"name1",
domain.IDPConfigTypeJWT,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
iam.NewIDPJWTConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
JWTEndpoint: "jwt-endpoint",
Issuer: "issuer",
KeysEndpoint: "keys-endpoint",
HeaderName: "auth",
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "idp config jwt add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
iam.NewIDPConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"name1",
domain.IDPConfigTypeJWT,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
iam.NewIDPJWTConfigAddedEvent(context.Background(),
&iam.NewAggregate().Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newDefaultIDPJWTConfigChangedEvent(context.Background(),
"config1",
"jwt-endpoint-changed",
"issuer-changed",
"keys-endpoint-changed",
"auth-changed",
),
),
},
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
JWTEndpoint: "jwt-endpoint-changed",
Issuer: "issuer-changed",
KeysEndpoint: "keys-endpoint-changed",
HeaderName: "auth-changed",
},
},
res: res{
want: &domain.JWTIDPConfig{
ObjectRoot: models.ObjectRoot{
AggregateID: "IAM",
ResourceOwner: "IAM",
},
IDPConfigID: "config1",
JWTEndpoint: "jwt-endpoint-changed",
Issuer: "issuer-changed",
KeysEndpoint: "keys-endpoint-changed",
HeaderName: "auth-changed",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
idpConfigSecretCrypto: tt.fields.secretCrypto,
}
got, err := r.ChangeDefaultIDPJWTConfig(tt.args.ctx, tt.args.config)
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 newDefaultIDPJWTConfigChangedEvent(ctx context.Context, configID, jwtEndpoint, issuer, keysEndpoint, headerName string) *iam.IDPJWTConfigChangedEvent {
event, _ := iam.NewIDPJWTConfigChangedEvent(ctx,
&iam.NewAggregate().Aggregate,
configID,
[]idpconfig.JWTConfigChanges{
idpconfig.ChangeJWTEndpoint(jwtEndpoint),
idpconfig.ChangeJWTIssuer(issuer),
idpconfig.ChangeKeysEndpoint(keysEndpoint),
idpconfig.ChangeHeaderName(headerName),
},
)
return event
}

View File

@@ -0,0 +1,61 @@
package command
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/idpconfig"
)
type JWTConfigWriteModel struct {
eventstore.WriteModel
IDPConfigID string
JWTEndpoint string
Issuer string
KeysEndpoint string
HeaderName string
State domain.IDPConfigState
}
func (wm *JWTConfigWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *idpconfig.JWTConfigAddedEvent:
wm.reduceConfigAddedEvent(e)
case *idpconfig.JWTConfigChangedEvent:
wm.reduceConfigChangedEvent(e)
case *idpconfig.IDPConfigDeactivatedEvent:
wm.State = domain.IDPConfigStateInactive
case *idpconfig.IDPConfigReactivatedEvent:
wm.State = domain.IDPConfigStateActive
case *idpconfig.IDPConfigRemovedEvent:
wm.State = domain.IDPConfigStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *JWTConfigWriteModel) reduceConfigAddedEvent(e *idpconfig.JWTConfigAddedEvent) {
wm.IDPConfigID = e.IDPConfigID
wm.JWTEndpoint = e.JWTEndpoint
wm.Issuer = e.Issuer
wm.KeysEndpoint = e.KeysEndpoint
wm.HeaderName = e.HeaderName
wm.State = domain.IDPConfigStateActive
}
func (wm *JWTConfigWriteModel) reduceConfigChangedEvent(e *idpconfig.JWTConfigChangedEvent) {
if e.JWTEndpoint != nil {
wm.JWTEndpoint = *e.JWTEndpoint
}
if e.Issuer != nil {
wm.Issuer = *e.Issuer
}
if e.KeysEndpoint != nil {
wm.KeysEndpoint = *e.KeysEndpoint
}
if e.HeaderName != nil {
wm.HeaderName = *e.HeaderName
}
}

View File

@@ -2,6 +2,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@@ -16,7 +17,7 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-0j8gs", "Errors.ResourceOwnerMissing")
}
if config.OIDCConfig == nil {
if config.OIDCConfig == nil && config.JWTConfig == nil {
return nil, errors.ThrowInvalidArgument(nil, "Org-eUpQU", "Errors.idp.config.notset")
}
@@ -26,11 +27,6 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r
}
addedConfig := NewOrgIDPConfigWriteModel(idpConfigID, resourceOwner)
clientSecret, err := crypto.Crypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto)
if err != nil {
return nil, err
}
orgAgg := OrgAggregateFromWriteModel(&addedConfig.WriteModel)
events := []eventstore.EventPusher{
org_repo.NewIDPConfigAddedEvent(
@@ -42,7 +38,13 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r
config.StylingType,
config.AutoRegister,
),
org_repo.NewIDPOIDCConfigAddedEvent(
}
if config.OIDCConfig != nil {
clientSecret, err := crypto.Crypt([]byte(config.OIDCConfig.ClientSecretString), c.idpConfigSecretCrypto)
if err != nil {
return nil, err
}
events = append(events, org_repo.NewIDPOIDCConfigAddedEvent(
ctx,
orgAgg,
config.OIDCConfig.ClientID,
@@ -53,7 +55,17 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r
clientSecret,
config.OIDCConfig.IDPDisplayNameMapping,
config.OIDCConfig.UsernameMapping,
config.OIDCConfig.Scopes...),
config.OIDCConfig.Scopes...))
} else if config.JWTConfig != nil {
events = append(events, org_repo.NewIDPJWTConfigAddedEvent(
ctx,
orgAgg,
idpConfigID,
config.JWTConfig.JWTEndpoint,
config.JWTConfig.Issuer,
config.JWTConfig.KeysEndpoint,
config.JWTConfig.HeaderName,
))
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {

View File

@@ -159,6 +159,66 @@ func TestCommandSide_AddIDPConfig(t *testing.T) {
},
},
},
{
name: "idp config jwt add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewIDPConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"name1",
domain.IDPConfigTypeOIDC,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
org.NewIDPJWTConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
},
uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name1", "org1")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "config1"),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
config: &domain.IDPConfig{
Name: "name1",
StylingType: domain.IDPConfigStylingTypeGoogle,
JWTConfig: &domain.JWTIDPConfig{
JWTEndpoint: "jwt-endpoint",
Issuer: "issuer",
KeysEndpoint: "keys-endpoint",
HeaderName: "auth",
},
},
},
res: res{
want: &domain.IDPConfig{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
ResourceOwner: "org1",
},
IDPConfigID: "config1",
Name: "name1",
StylingType: domain.IDPConfigStylingTypeGoogle,
State: domain.IDPConfigStateActive,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -0,0 +1,53 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
)
func (c *Commands) ChangeIDPJWTConfig(ctx context.Context, config *domain.JWTIDPConfig, resourceOwner string) (*domain.JWTIDPConfig, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-ff8NF", "Errors.ResourceOwnerMissing")
}
if config.IDPConfigID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-2n99f", "Errors.IDMissing")
}
existingConfig := NewOrgIDPJWTConfigWriteModel(config.IDPConfigID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, existingConfig)
if err != nil {
return nil, err
}
if existingConfig.State == domain.IDPConfigStateRemoved || existingConfig.State == domain.IDPConfigStateUnspecified {
return nil, caos_errs.ThrowNotFound(nil, "Org-67J9d", "Errors.Org.IDPConfig.AlreadyExists")
}
orgAgg := OrgAggregateFromWriteModel(&existingConfig.WriteModel)
changedEvent, hasChanged, err := existingConfig.NewChangedEvent(
ctx,
orgAgg,
config.IDPConfigID,
config.JWTEndpoint,
config.Issuer,
config.KeysEndpoint,
config.HeaderName)
if err != nil {
return nil, err
}
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-2k9fs", "Errors.Org.IDPConfig.NotChanged")
}
pushedEvents, err := c.eventstore.PushEvents(ctx, changedEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingConfig, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToIDPJWTConfig(&existingConfig.JWTConfigWriteModel), nil
}

View File

@@ -0,0 +1,115 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/idpconfig"
"github.com/caos/zitadel/internal/repository/org"
)
type IDPJWTConfigWriteModel struct {
JWTConfigWriteModel
}
func NewOrgIDPJWTConfigWriteModel(idpConfigID, orgID string) *IDPJWTConfigWriteModel {
return &IDPJWTConfigWriteModel{
JWTConfigWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
IDPConfigID: idpConfigID,
},
}
}
func (wm *IDPJWTConfigWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *org.IDPJWTConfigAddedEvent:
if wm.IDPConfigID != e.IDPConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigAddedEvent)
case *org.IDPJWTConfigChangedEvent:
if wm.IDPConfigID != e.IDPConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.JWTConfigChangedEvent)
case *org.IDPConfigReactivatedEvent:
if wm.IDPConfigID != e.ConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigReactivatedEvent)
case *org.IDPConfigDeactivatedEvent:
if wm.IDPConfigID != e.ConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigDeactivatedEvent)
case *org.IDPConfigRemovedEvent:
if wm.IDPConfigID != e.ConfigID {
continue
}
wm.JWTConfigWriteModel.AppendEvents(&e.IDPConfigRemovedEvent)
default:
wm.JWTConfigWriteModel.AppendEvents(e)
}
}
}
func (wm *IDPJWTConfigWriteModel) Reduce() error {
if err := wm.JWTConfigWriteModel.Reduce(); err != nil {
return err
}
return wm.WriteModel.Reduce()
}
func (wm *IDPJWTConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(org.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
org.IDPJWTConfigAddedEventType,
org.IDPJWTConfigChangedEventType,
org.IDPConfigReactivatedEventType,
org.IDPConfigDeactivatedEventType,
org.IDPConfigRemovedEventType).
Builder()
}
func (wm *IDPJWTConfigWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
idpConfigID,
jwtEndpoint,
issuer,
keysEndpoint,
headerName string,
) (*org.IDPJWTConfigChangedEvent, bool, error) {
changes := make([]idpconfig.JWTConfigChanges, 0)
if wm.JWTEndpoint != jwtEndpoint {
changes = append(changes, idpconfig.ChangeJWTEndpoint(jwtEndpoint))
}
if wm.Issuer != issuer {
changes = append(changes, idpconfig.ChangeJWTIssuer(issuer))
}
if wm.KeysEndpoint != keysEndpoint {
changes = append(changes, idpconfig.ChangeKeysEndpoint(keysEndpoint))
}
if wm.HeaderName != headerName {
changes = append(changes, idpconfig.ChangeHeaderName(headerName))
}
if len(changes) == 0 {
return nil, false, nil
}
changeEvent, err := org.NewIDPJWTConfigChangedEvent(ctx, aggregate, idpConfigID, changes)
if err != nil {
return nil, false, err
}
return changeEvent, true, nil
}

View File

@@ -0,0 +1,288 @@
package command
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/repository/idpconfig"
"github.com/caos/zitadel/internal/repository/org"
)
func TestCommandSide_ChangeIDPJWTConfig(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type (
args struct {
ctx context.Context
config *domain.JWTIDPConfig
resourceOwner string
}
)
type res struct {
want *domain.JWTIDPConfig
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "resourceowner missing, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "invalid config, error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{},
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "idp config not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
},
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "idp config removed, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewIDPConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"name1",
domain.IDPConfigTypeJWT,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
org.NewIDPJWTConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
eventFromEventPusher(
org.NewIDPConfigRemovedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"name",
),
),
),
),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
},
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewIDPConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"name1",
domain.IDPConfigTypeJWT,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
org.NewIDPJWTConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
JWTEndpoint: "jwt-endpoint",
Issuer: "issuer",
KeysEndpoint: "keys-endpoint",
HeaderName: "auth",
},
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "idp config jwt add, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewIDPConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"name1",
domain.IDPConfigTypeJWT,
domain.IDPConfigStylingTypeGoogle,
false,
),
),
eventFromEventPusher(
org.NewIDPJWTConfigAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
"config1",
"jwt-endpoint",
"issuer",
"keys-endpoint",
"auth",
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newIDPJWTConfigChangedEvent(context.Background(),
"org1",
"config1",
"jwt-endpoint-changed",
"issuer-changed",
"keys-endpoint-changed",
"auth-changed",
),
),
},
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
config: &domain.JWTIDPConfig{
IDPConfigID: "config1",
JWTEndpoint: "jwt-endpoint-changed",
Issuer: "issuer-changed",
KeysEndpoint: "keys-endpoint-changed",
HeaderName: "auth-changed",
},
resourceOwner: "org1",
},
res: res{
want: &domain.JWTIDPConfig{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
ResourceOwner: "org1",
},
IDPConfigID: "config1",
JWTEndpoint: "jwt-endpoint-changed",
Issuer: "issuer-changed",
KeysEndpoint: "keys-endpoint-changed",
HeaderName: "auth-changed",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
idpConfigSecretCrypto: tt.fields.secretCrypto,
}
got, err := r.ChangeIDPJWTConfig(tt.args.ctx, tt.args.config, 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 newIDPJWTConfigChangedEvent(ctx context.Context, orgID, configID, jwtEndpoint, issuer, keysEndpoint, headerName string) *org.IDPJWTConfigChangedEvent {
event, _ := org.NewIDPJWTConfigChangedEvent(ctx,
&org.NewAggregate(orgID, orgID).Aggregate,
configID,
[]idpconfig.JWTConfigChanges{
idpconfig.ChangeJWTEndpoint(jwtEndpoint),
idpconfig.ChangeJWTIssuer(issuer),
idpconfig.ChangeKeysEndpoint(keysEndpoint),
idpconfig.ChangeHeaderName(headerName),
},
)
return event
}