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:
Gayathri Vijayan
2026-04-13 15:05:36 +00:00
committed by GitHub
co-authored by Copilot
parent 01fe34a526
commit bb3b52dda3
14 changed files with 906 additions and 23 deletions
+11
View File
@@ -466,3 +466,14 @@ func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProvide
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) AddZitadelProvider(ctx context.Context, req *admin_pb.AddZitadelProviderRequest) (*admin_pb.AddZitadelProviderResponse, error) {
id, details, err := s.command.AddInstanceZitadelProvider(ctx, addZitadelProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddZitadelProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
+22
View File
@@ -1,6 +1,8 @@
package admin
import (
"strings"
"github.com/crewjam/saml"
"github.com/muhlemmer/gu"
dsig "github.com/russellhaering/goxmldsig"
@@ -11,6 +13,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/zerrors"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
@@ -545,3 +548,22 @@ func signatureAlgorithmToCommand(signatureAlgorithm idp_pb.SAMLSignatureAlgorith
return ""
}
}
func addZitadelProviderToCommand(req *admin_pb.AddZitadelProviderRequest) command.ZitadelProvider {
instanceRolesInfo := make([]idp.RolesInfo, 0, len(req.InstanceRolesInfo))
for _, info := range req.InstanceRolesInfo {
instanceRolesInfo = append(instanceRolesInfo, idp.RolesInfo{
OrganizationID: strings.TrimSpace(info.OrganizationId),
OrganizationDomain: strings.TrimSpace(info.OrganizationDomain),
})
}
return command.ZitadelProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
InstanceRolesInfo: instanceRolesInfo,
}
}
+82 -13
View File
@@ -3,11 +3,14 @@ package admin
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/test"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/idp"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
)
func Test_addOIDCIDPRequestToDomain(t *testing.T) {
@@ -23,13 +26,13 @@ func Test_addOIDCIDPRequestToDomain(t *testing.T) {
args: args{
req: &admin_pb.AddOIDCIDPRequest{
Name: "ZITADEL",
StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE,
StylingType: idp_pb.IDPStylingType_STYLING_TYPE_GOOGLE,
ClientId: "test1234",
ClientSecret: "test4321",
Issuer: "zitadel.ch",
Scopes: []string{"email", "profile"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
DisplayNameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
AutoRegister: true,
},
},
@@ -70,8 +73,8 @@ func Test_addOIDCIDPRequestToDomainOIDCIDPConfig(t *testing.T) {
ClientSecret: "test4321",
Issuer: "zitadel.ch",
Scopes: []string{"email", "profile"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
DisplayNameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
},
},
},
@@ -104,7 +107,7 @@ func Test_updateIDPToDomain(t *testing.T) {
req: &admin_pb.UpdateIDPRequest{
IdpId: "13523",
Name: "new name",
StylingType: idp.IDPStylingType_STYLING_TYPE_GOOGLE,
StylingType: idp_pb.IDPStylingType_STYLING_TYPE_GOOGLE,
AutoRegister: true,
},
},
@@ -141,8 +144,8 @@ func Test_updateOIDCConfigToDomain(t *testing.T) {
ClientId: "ZITEADEL",
ClientSecret: "i'm so secret",
Scopes: []string{"profile"},
DisplayNameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
DisplayNameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_EMAIL,
UsernameMapping: idp_pb.OIDCMappingField_OIDC_MAPPING_FIELD_PREFERRED_USERNAME,
},
},
},
@@ -164,7 +167,7 @@ func Test_signatureAlgorithmToCommand(t *testing.T) {
t.Parallel()
tests := []struct {
name string
signatureAlgorithm idp.SAMLSignatureAlgorithm
signatureAlgorithm idp_pb.SAMLSignatureAlgorithm
wantSignatureAlgorithm string
}{
{
@@ -174,17 +177,17 @@ func Test_signatureAlgorithmToCommand(t *testing.T) {
},
{
name: "RSA_SHA1",
signatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA1,
signatureAlgorithm: idp_pb.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA1,
wantSignatureAlgorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
},
{
name: "RSA_SHA256",
signatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA256,
signatureAlgorithm: idp_pb.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA256,
wantSignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
},
{
name: "RSA_SHA512",
signatureAlgorithm: idp.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA512,
signatureAlgorithm: idp_pb.SAMLSignatureAlgorithm_SAML_SIGNATURE_RSA_SHA512,
wantSignatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
},
}
@@ -196,3 +199,69 @@ func Test_signatureAlgorithmToCommand(t *testing.T) {
})
}
}
func Test_addZitadelProviderToCommand(t *testing.T) {
t.Parallel()
tests := []struct {
name string
req *admin_pb.AddZitadelProviderRequest
want command.ZitadelProvider
}{
{
name: "all fields filled",
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"},
ProviderOptions: &idp_pb.Options{
IsLinkingAllowed: false,
IsCreationAllowed: true,
IsAutoCreation: false,
IsAutoUpdate: true,
AutoLinking: 0,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationId: "org2",
OrganizationDomain: "org2.com",
},
},
},
want: command.ZitadelProvider{
Name: "Zitadel Support IdP",
ClientID: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsAutoCreation: false,
IsLinkingAllowed: false,
IsAutoUpdate: true,
},
InstanceRolesInfo: []idp.RolesInfo{
{
OrganizationID: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationID: "org2",
OrganizationDomain: "org2.com",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := addZitadelProviderToCommand(tt.req)
assert.Equal(t, tt.want, got)
})
}
}
@@ -0,0 +1,198 @@
//go:build integration
package admin_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
object_pb "github.com/zitadel/zitadel/pkg/grpc/object"
)
func Test_AddZitadelProvider(t *testing.T) {
type args struct {
ctx context.Context
req *admin_pb.AddZitadelProviderRequest
}
tests := []struct {
name string
args args
wantErr error
wantResponse *admin_pb.AddZitadelProviderResponse
}{
{
name: "missing required field: name",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{},
},
wantErr: status.Error(codes.InvalidArgument, "invalid AddZitadelProviderRequest.Name: value length must be between 1 and 200 runes, inclusive"),
},
{
name: "missing required field: issuer",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
},
},
wantErr: status.Error(codes.InvalidArgument, "invalid AddZitadelProviderRequest.Issuer: value length must be between 1 and 200 runes, inclusive"),
},
{
name: "missing required field: client_id",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
},
},
wantErr: status.Error(codes.InvalidArgument, "invalid AddZitadelProviderRequest.ClientId: value length must be between 1 and 200 runes, inclusive"),
},
{
name: "missing required field: client_secret",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
},
},
wantErr: status.Error(
codes.InvalidArgument,
"invalid AddZitadelProviderRequest.ClientSecret: value length must be between 1 and 1000 runes, inclusive",
),
},
{
name: "missing org ID in instance roles info",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "",
},
},
},
},
wantErr: status.Error(
codes.InvalidArgument,
"invalid AddZitadelProviderRequest.InstanceRolesInfo[0]: embedded message failed validation | caused by: invalid InstanceRolesInfo.OrganizationId: value length must be between 1 and 200 runes, inclusive",
),
},
{
name: "missing org domain in instance roles info",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationId: "org2",
},
},
},
},
wantErr: status.Error(
codes.InvalidArgument,
"invalid AddZitadelProviderRequest.InstanceRolesInfo[1]: embedded message failed validation | caused by: invalid InstanceRolesInfo.OrganizationDomain: value length must be between 1 and 200 runes, inclusive",
),
},
{
name: "valid request without instance roles info",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
},
},
wantResponse: &admin_pb.AddZitadelProviderResponse{
Details: &object_pb.ObjectDetails{
ResourceOwner: Instance.Instance.Id,
},
},
},
{
name: "valid request with instance roles info",
args: args{
ctx: AdminCTX,
req: &admin_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
wantResponse: &admin_pb.AddZitadelProviderResponse{
Details: &object_pb.ObjectDetails{
ResourceOwner: Instance.Instance.Id,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
before := time.Now()
got, err := Client.AddZitadelProvider(tt.args.ctx, tt.args.req)
after := time.Now()
if tt.wantErr != nil {
require.Error(t, err)
grpcStatus, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.Code(tt.wantErr), grpcStatus.Code())
assert.Equal(t, status.Convert(tt.wantErr).Message(), grpcStatus.Message())
return
}
require.NoError(t, err)
assert.NotNil(t, got)
assert.NotEmpty(t, got.GetId())
assert.WithinRange(t, got.GetDetails().GetCreationDate().AsTime(), before, after)
assert.Equal(t, tt.wantResponse.GetDetails().GetResourceOwner(), got.GetDetails().GetResourceOwner())
})
}
}
+10
View File
@@ -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)
+43
View File
@@ -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
}
+86
View File
@@ -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
}
+40
View File
@@ -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()
}
+309
View File
@@ -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)
}
})
}
}
+61
View File
@@ -0,0 +1,61 @@
package idp
import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
)
type RolesInfo struct {
OrganizationID string `json:"organizationId"`
OrganizationDomain string `json:"organizationDomain"`
}
type ZitadelIDPAddedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Name string `json:"name"`
Issuer string `json:"issuer"`
ClientID string `json:"clientId"`
ClientSecret *crypto.CryptoValue `json:"clientSecret"`
Scopes []string `json:"scopes,omitempty"`
InstanceRolesInfo []RolesInfo `json:"instanceRolesInfo,omitempty"`
Options
}
func NewZitadelIDPAddedEvent(
base *eventstore.BaseEvent,
id,
name,
issuer,
clientID string,
clientSecret *crypto.CryptoValue,
scopes []string,
options Options,
instanceRolesInfo []RolesInfo,
) *ZitadelIDPAddedEvent {
return &ZitadelIDPAddedEvent{
BaseEvent: *base,
ID: id,
Name: name,
Issuer: issuer,
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: scopes,
Options: options,
InstanceRolesInfo: instanceRolesInfo,
}
}
func (e *ZitadelIDPAddedEvent) Payload() interface{} {
return e
}
func (e *ZitadelIDPAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
type ZitadelIDPChangedEvent struct {
eventstore.BaseEvent `json:"-"`
// todo (@grvijayan): will be added along with UpdateZitadelProvider changes
}
@@ -105,6 +105,7 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SAMLIDPAddedEventType, SAMLIDPAddedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, SAMLIDPChangedEventType, SAMLIDPChangedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, ZitadelIDPAddedEventType, eventstore.GenericEventMapper[ZitadelIDPAddedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderCascadeRemovedEventType, IdentityProviderCascadeRemovedEventMapper)
+41
View File
@@ -38,6 +38,7 @@ const (
SAMLIDPAddedEventType eventstore.EventType = "instance.idp.saml.added"
SAMLIDPChangedEventType eventstore.EventType = "instance.idp.saml.changed"
IDPRemovedEventType eventstore.EventType = "instance.idp.removed"
ZitadelIDPAddedEventType eventstore.EventType = "instance.idp.zitadel.added"
)
type OAuthIDPAddedEvent struct {
@@ -1128,3 +1129,43 @@ func IDPRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &IDPRemovedEvent{RemovedEvent: *e.(*idp.RemovedEvent)}, nil
}
type ZitadelIDPAddedEvent struct {
idp.ZitadelIDPAddedEvent
}
func NewZitadelIDPAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
issuer,
clientID string,
clientSecret *crypto.CryptoValue,
scopes []string,
options idp.Options,
instanceRolesInfo []idp.RolesInfo,
) *ZitadelIDPAddedEvent {
return &ZitadelIDPAddedEvent{
ZitadelIDPAddedEvent: *idp.NewZitadelIDPAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
ZitadelIDPAddedEventType,
),
id,
name,
issuer,
clientID,
clientSecret,
scopes,
options,
instanceRolesInfo,
),
}
}
func (e *ZitadelIDPAddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
e.BaseEvent = *event
}
+1 -5
View File
@@ -7338,13 +7338,9 @@ message AddZitadelProviderRequest {
];
zitadel.idp.v1.Options provider_options = 6;
bool is_id_token_mapping = 7;
// Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow.
bool use_pkce = 8;
// Optionally specify the details (organization_id, organization_domain) related to instance roles.
// It is used to determine if a user should be assigned instance administrator roles after authenticating with the provider.
repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 9 [(validate.rules).repeated = {max_items: 20}];
repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 7 [(validate.rules).repeated = {max_items: 20}];
}
message AddZitadelProviderResponse {
+1 -5
View File
@@ -14087,13 +14087,9 @@ message AddZitadelProviderRequest {
];
zitadel.idp.v1.Options provider_options = 6;
bool is_id_token_mapping = 7;
// Enable the use of Proof Key for Code Exchange (PKCE) for the OIDC flow.
bool use_pkce = 8;
// Optionally specify the details (organization_id, organization_domain) related to instance roles.
// It is used to determine if a user should be assigned instance administrator roles after authenticating with the provider.
repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 9 [(validate.rules).repeated = {max_items: 20}];
repeated zitadel.idp.v1.InstanceRolesInfo instance_roles_info = 7 [(validate.rules).repeated = {max_items: 20}];
}
message AddZitadelProviderResponse {