mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
feat: command layer implementation for AddZitadelProvider (#12020)
# Which Problems Are Solved This PR contains the command-layer implementation to add an instance IDP of the type `ZitadelProvider` # How the Problems Are Solved - Implementing `AddZitadelProvider` in AdminService - Adding the command-layer to create a `ZitadelProvider` and push `ZitadelIDPAddedEvent` # Additional Changes N/A # Additional Context - Related to https://github.com/zitadel/zitadel/issues/11823 - Follow-up for PR https://github.com/zitadel/zitadel/pull/12018 --------- Co-authored-by: Copilot <[email protected]>
This commit is contained in:
co-authored by
Copilot
parent
01fe34a526
commit
bb3b52dda3
@@ -137,6 +137,16 @@ type AppleProvider struct {
|
||||
IDPOptions idp.Options
|
||||
}
|
||||
|
||||
type ZitadelProvider struct {
|
||||
Name string
|
||||
Issuer string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
Scopes []string
|
||||
IDPOptions idp.Options
|
||||
InstanceRolesInfo []idp.RolesInfo
|
||||
}
|
||||
|
||||
// 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) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
|
||||
@@ -2322,3 +2322,46 @@ func (wm *AllIDPWriteModel) ToSAMLProvider(callbackURL string, idpAlg crypto.Enc
|
||||
}
|
||||
return wm.samlModel.ToProvider(callbackURL, idpAlg, getRequest, addRequest)
|
||||
}
|
||||
|
||||
type ZitadelIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
Name string
|
||||
ID string
|
||||
Issuer string
|
||||
ClientID string
|
||||
ClientSecret *crypto.CryptoValue
|
||||
Scopes []string
|
||||
idp.Options
|
||||
InstanceRolesInfo []idp.RolesInfo
|
||||
|
||||
State domain.IDPState
|
||||
}
|
||||
|
||||
func (wm *ZitadelIDPWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *idp.ZitadelIDPAddedEvent:
|
||||
wm.reduceAddedEvent(e)
|
||||
case *idp.ZitadelIDPChangedEvent:
|
||||
// todo (@grvijayan): to please the linter for now
|
||||
wm.reduceChangedEvent(e)
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *ZitadelIDPWriteModel) reduceAddedEvent(e *idp.ZitadelIDPAddedEvent) {
|
||||
wm.Name = e.Name
|
||||
wm.Issuer = e.Issuer
|
||||
wm.ClientID = e.ClientID
|
||||
wm.ClientSecret = e.ClientSecret
|
||||
wm.Scopes = e.Scopes
|
||||
wm.Options = e.Options
|
||||
wm.InstanceRolesInfo = e.InstanceRolesInfo
|
||||
wm.State = domain.IDPStateActive
|
||||
}
|
||||
|
||||
func (wm *ZitadelIDPWriteModel) reduceChangedEvent(e *idp.ZitadelIDPChangedEvent) {
|
||||
// todo (@grvijayan): will be implemented along with UpdateZitadelProvider changes
|
||||
}
|
||||
|
||||
@@ -1937,3 +1937,89 @@ func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id strin
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) AddInstanceZitadelProvider(ctx context.Context, provider ZitadelProvider) (string, *domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
instanceAgg := instance.NewAggregate(instanceID)
|
||||
id, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = c.validateZitadelProvider(ctx, &provider)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
writeModel := NewInstanceZitadelIDPWriteModel(instanceID, id)
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceZitadelProvider(instanceAgg, writeModel, provider)) //nolint:staticcheck
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return id, pushedEventsToObjectDetails(pushedEvents), nil
|
||||
}
|
||||
|
||||
func (c *Commands) prepareAddInstanceZitadelProvider(a *instance.Aggregate, writeModel *InstanceZitadelIDPWriteModel, provider ZitadelProvider) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
if err = writeModel.Reduce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret, err := crypto.Encrypt([]byte(provider.ClientSecret), c.idpConfigEncryption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []eventstore.Command{
|
||||
instance.NewZitadelIDPAddedEvent(
|
||||
ctx,
|
||||
&a.Aggregate,
|
||||
writeModel.ID,
|
||||
provider.Name,
|
||||
provider.Issuer,
|
||||
provider.ClientID,
|
||||
secret,
|
||||
provider.Scopes,
|
||||
provider.IDPOptions,
|
||||
provider.InstanceRolesInfo,
|
||||
),
|
||||
}, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) validateZitadelProvider(ctx context.Context, provider *ZitadelProvider) error {
|
||||
if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-Sgtj5", "Errors.Invalid.Argument")
|
||||
}
|
||||
if provider.Issuer = strings.TrimSpace(provider.Issuer); provider.Issuer == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-Hz6zj", "Errors.Invalid.Argument")
|
||||
}
|
||||
if provider.ClientID = strings.TrimSpace(provider.ClientID); provider.ClientID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-fb5jm", "Errors.Invalid.Argument")
|
||||
}
|
||||
if provider.ClientSecret = strings.TrimSpace(provider.ClientSecret); provider.ClientSecret == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-Sfdf4", "Errors.Invalid.Argument")
|
||||
}
|
||||
|
||||
for i := range provider.InstanceRolesInfo {
|
||||
provider.InstanceRolesInfo[i].OrganizationID = strings.TrimSpace(provider.InstanceRolesInfo[i].OrganizationID)
|
||||
if provider.InstanceRolesInfo[i].OrganizationID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-9uEJaM", "Errors.Invalid.Argument")
|
||||
}
|
||||
provider.InstanceRolesInfo[i].OrganizationDomain = strings.TrimSpace(provider.InstanceRolesInfo[i].OrganizationDomain)
|
||||
if provider.InstanceRolesInfo[i].OrganizationDomain == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-y4Ib4D", "Errors.Invalid.Argument")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1034,3 +1034,43 @@ func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
EventData(map[string]interface{}{"idpConfigId": wm.ID}).
|
||||
Builder()
|
||||
}
|
||||
|
||||
type InstanceZitadelIDPWriteModel struct {
|
||||
ZitadelIDPWriteModel
|
||||
}
|
||||
|
||||
func NewInstanceZitadelIDPWriteModel(instanceID, id string) *InstanceZitadelIDPWriteModel {
|
||||
return &InstanceZitadelIDPWriteModel{
|
||||
ZitadelIDPWriteModel: ZitadelIDPWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: instanceID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
ID: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *InstanceZitadelIDPWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *instance.ZitadelIDPAddedEvent:
|
||||
wm.ZitadelIDPWriteModel.AppendEvents(&e.ZitadelIDPAddedEvent)
|
||||
default:
|
||||
wm.ZitadelIDPWriteModel.AppendEvents(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *InstanceZitadelIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(instance.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
instance.ZitadelIDPAddedEventType,
|
||||
).
|
||||
EventData(map[string]interface{}{"id": wm.ID}).
|
||||
Builder()
|
||||
}
|
||||
|
||||
@@ -5937,3 +5937,312 @@ func TestCommandSide_RegenerateInstanceSAMLProviderCertificate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSide_AddInstanceZitadelIDP(t *testing.T) {
|
||||
pushErr := errors.New("push error")
|
||||
type fields struct {
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
secretCrypto crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
provider ZitadelProvider
|
||||
}
|
||||
type res struct {
|
||||
id string
|
||||
want *domain.ObjectDetails
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"invalid name",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
provider: ZitadelProvider{},
|
||||
},
|
||||
res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Sgtj5", ""))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid issuer",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
provider: ZitadelProvider{
|
||||
Name: "name",
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Hz6zj", ""))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid clientID",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
provider: ZitadelProvider{
|
||||
Name: "name",
|
||||
Issuer: "issuer",
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-fb5jm", ""))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid clientSecret",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
provider: ZitadelProvider{
|
||||
Name: "name",
|
||||
Issuer: "issuer",
|
||||
ClientID: "clientID",
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "INST-Sfdf4", ""))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPushFailed(
|
||||
pushErr,
|
||||
instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
|
||||
"id1",
|
||||
"name",
|
||||
"issuer",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
nil,
|
||||
idp.Options{},
|
||||
[]idp.RolesInfo{
|
||||
{
|
||||
OrganizationID: "org1",
|
||||
OrganizationDomain: "example-org1.com",
|
||||
},
|
||||
{
|
||||
OrganizationID: "org2",
|
||||
OrganizationDomain: "example-org2.com",
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
|
||||
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
provider: ZitadelProvider{
|
||||
Name: "name",
|
||||
Issuer: "issuer",
|
||||
ClientID: "clientID",
|
||||
ClientSecret: "clientSecret",
|
||||
InstanceRolesInfo: []idp.RolesInfo{
|
||||
{
|
||||
OrganizationID: "org1",
|
||||
OrganizationDomain: "example-org1.com",
|
||||
},
|
||||
{
|
||||
OrganizationID: "org2",
|
||||
OrganizationDomain: "example-org2.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, pushErr)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
|
||||
"id1",
|
||||
"name",
|
||||
"issuer",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
nil,
|
||||
idp.Options{},
|
||||
[]idp.RolesInfo{
|
||||
{
|
||||
OrganizationID: "org1",
|
||||
OrganizationDomain: "example-org1.com",
|
||||
},
|
||||
{
|
||||
OrganizationID: "org2",
|
||||
OrganizationDomain: "example-org2.com",
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
|
||||
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
provider: ZitadelProvider{
|
||||
Name: "name",
|
||||
Issuer: "issuer",
|
||||
ClientID: "clientID",
|
||||
ClientSecret: "clientSecret",
|
||||
InstanceRolesInfo: []idp.RolesInfo{
|
||||
{
|
||||
OrganizationID: "org1",
|
||||
OrganizationDomain: "example-org1.com",
|
||||
},
|
||||
{
|
||||
OrganizationID: "org2",
|
||||
OrganizationDomain: "example-org2.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
id: "id1",
|
||||
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok all set",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
instance.NewZitadelIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
|
||||
"id1",
|
||||
"name",
|
||||
"issuer",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
[]string{openid.ScopeOpenID},
|
||||
idp.Options{
|
||||
IsCreationAllowed: true,
|
||||
IsLinkingAllowed: true,
|
||||
IsAutoCreation: true,
|
||||
IsAutoUpdate: true,
|
||||
},
|
||||
[]idp.RolesInfo{
|
||||
{
|
||||
OrganizationID: "org1",
|
||||
OrganizationDomain: "example-org1.com",
|
||||
},
|
||||
{
|
||||
OrganizationID: "org2",
|
||||
OrganizationDomain: "example-org2.com",
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
|
||||
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instance1"),
|
||||
provider: ZitadelProvider{
|
||||
Name: "name",
|
||||
Issuer: "issuer",
|
||||
ClientID: "clientID",
|
||||
ClientSecret: "clientSecret",
|
||||
Scopes: []string{openid.ScopeOpenID},
|
||||
IDPOptions: idp.Options{
|
||||
IsCreationAllowed: true,
|
||||
IsLinkingAllowed: true,
|
||||
IsAutoCreation: true,
|
||||
IsAutoUpdate: true,
|
||||
},
|
||||
InstanceRolesInfo: []idp.RolesInfo{
|
||||
{
|
||||
OrganizationID: "org1",
|
||||
OrganizationDomain: "example-org1.com",
|
||||
},
|
||||
{
|
||||
OrganizationID: "org2",
|
||||
OrganizationDomain: "example-org2.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
id: "id1",
|
||||
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
idpConfigEncryption: tt.fields.secretCrypto,
|
||||
}
|
||||
idpId, got, err := c.AddInstanceZitadelProvider(tt.args.ctx, tt.args.provider)
|
||||
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.id, idpId)
|
||||
assertObjectDetails(t, tt.res.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user