feat: add management for ldap idp template (#5220)

Add management functionality for LDAP idps with templates and the basic functionality for the LDAP provider, which can then be used with a separate login page in the future.

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2023-02-15 09:14:59 +01:00
committed by GitHub
parent 058192c22b
commit 586495a0be
37 changed files with 7298 additions and 14 deletions

View File

@@ -150,3 +150,57 @@ func (s *Server) UpdateIDPJWTConfig(ctx context.Context, req *admin_pb.UpdateIDP
),
}, nil
}
func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderByIDRequest) (*admin_pb.GetProviderByIDResponse, error) {
idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetInstance(ctx).InstanceID(), false)
if err != nil {
return nil, err
}
return &admin_pb.GetProviderByIDResponse{Idp: idp_grpc.ProviderToPb(idp)}, nil
}
func (s *Server) ListProviders(ctx context.Context, req *admin_pb.ListProvidersRequest) (*admin_pb.ListProvidersResponse, error) {
queries, err := listProvidersToQuery(authz.GetInstance(ctx).InstanceID(), req)
if err != nil {
return nil, err
}
resp, err := s.query.IDPTemplates(ctx, queries, false)
if err != nil {
return nil, err
}
return &admin_pb.ListProvidersResponse{
Result: idp_grpc.ProvidersToPb(resp.Templates),
Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.Timestamp),
}, nil
}
func (s *Server) AddLDAPProvider(ctx context.Context, req *admin_pb.AddLDAPProviderRequest) (*admin_pb.AddLDAPProviderResponse, error) {
id, details, err := s.command.AddInstanceLDAPProvider(ctx, addLDAPProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.AddLDAPProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateLDAPProvider(ctx context.Context, req *admin_pb.UpdateLDAPProviderRequest) (*admin_pb.UpdateLDAPProviderResponse, error) {
details, err := s.command.UpdateInstanceLDAPProvider(ctx, req.Id, updateLDAPProviderToCommand(req))
if err != nil {
return nil, err
}
return &admin_pb.UpdateLDAPProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *admin_pb.DeleteProviderRequest) (*admin_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteInstanceProvider(ctx, req.Id)
if err != nil {
return nil, err
}
return &admin_pb.DeleteProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}

View File

@@ -3,6 +3,7 @@ package admin
import (
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -155,3 +156,79 @@ func idpUserLinksToDomain(idps []*query.IDPUserLink) []*domain.UserIDPLink {
}
return externalIDPs
}
func listProvidersToQuery(instanceID string, req *admin_pb.ListProvidersRequest) (*query.IDPTemplateSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := providerQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
iamQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(instanceID)
if err != nil {
return nil, err
}
queries = append(queries, iamQuery)
return &query.IDPTemplateSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func providerQueriesToQuery(queries []*admin_pb.ProviderQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = providerQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func providerQueryToQuery(idpQuery *admin_pb.ProviderQuery) (query.SearchQuery, error) {
switch q := idpQuery.Query.(type) {
case *admin_pb.ProviderQuery_IdpNameQuery:
return query.NewIDPTemplateNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
case *admin_pb.ProviderQuery_IdpIdQuery:
return query.NewIDPTemplateIDSearchQuery(q.IdpIdQuery.Id)
default:
return nil, errors.ThrowInvalidArgument(nil, "ADMIN-Dr2aa", "List.Query.Invalid")
}
}
func addLDAPProviderToCommand(req *admin_pb.AddLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Host: req.Host,
Port: req.Port,
TLS: req.Tls,
BaseDN: req.BaseDn,
UserObjectClass: req.UserObjectClass,
UserUniqueAttribute: req.UserUniqueAttribute,
Admin: req.Admin,
Password: req.Password,
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateLDAPProviderToCommand(req *admin_pb.UpdateLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Host: req.Host,
Port: req.Port,
TLS: req.Tls,
BaseDN: req.BaseDn,
UserObjectClass: req.UserObjectClass,
UserUniqueAttribute: req.UserUniqueAttribute,
Admin: req.Admin,
Password: req.Password,
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
iam_model "github.com/zitadel/zitadel/internal/iam/model"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/idp"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
)
@@ -296,3 +297,142 @@ func ownerTypeToPB(typ domain.IdentityProviderType) idp_pb.IDPOwnerType {
return idp_pb.IDPOwnerType_IDP_OWNER_TYPE_UNSPECIFIED
}
}
func OptionsToCommand(options *idp_pb.Options) idp.Options {
if options == nil {
return idp.Options{}
}
return idp.Options{
IsCreationAllowed: options.IsCreationAllowed,
IsLinkingAllowed: options.IsLinkingAllowed,
IsAutoCreation: options.IsAutoCreation,
IsAutoUpdate: options.IsAutoUpdate,
}
}
func LDAPAttributesToCommand(attributes *idp_pb.LDAPAttributes) idp.LDAPAttributes {
if attributes == nil {
return idp.LDAPAttributes{}
}
return idp.LDAPAttributes{
IDAttribute: attributes.IdAttribute,
FirstNameAttribute: attributes.FirstNameAttribute,
LastNameAttribute: attributes.LastNameAttribute,
DisplayNameAttribute: attributes.DisplayNameAttribute,
NickNameAttribute: attributes.NickNameAttribute,
PreferredUsernameAttribute: attributes.PreferredUsernameAttribute,
EmailAttribute: attributes.EmailAttribute,
EmailVerifiedAttribute: attributes.EmailVerifiedAttribute,
PhoneAttribute: attributes.PhoneAttribute,
PhoneVerifiedAttribute: attributes.PhoneVerifiedAttribute,
PreferredLanguageAttribute: attributes.PreferredLanguageAttribute,
AvatarURLAttribute: attributes.AvatarUrlAttribute,
ProfileAttribute: attributes.ProfileAttribute,
}
}
func ProvidersToPb(providers []*query.IDPTemplate) []*idp_pb.Provider {
list := make([]*idp_pb.Provider, len(providers))
for i, provider := range providers {
list[i] = ProviderToPb(provider)
}
return list
}
func ProviderToPb(provider *query.IDPTemplate) *idp_pb.Provider {
return &idp_pb.Provider{
Id: provider.ID,
Details: obj_grpc.ToViewDetailsPb(provider.Sequence, provider.CreationDate, provider.ChangeDate, provider.ResourceOwner),
State: providerStateToPb(provider.State),
Name: provider.Name,
Owner: ownerTypeToPB(provider.OwnerType),
Type: providerTypeToPb(provider.Type),
Config: configToPb(provider),
}
}
func providerStateToPb(state domain.IDPState) idp_pb.IDPState {
switch state { //nolint:exhaustive
case domain.IDPStateActive:
return idp_pb.IDPState_IDP_STATE_ACTIVE
case domain.IDPStateInactive:
return idp_pb.IDPState_IDP_STATE_INACTIVE
case domain.IDPStateUnspecified:
return idp_pb.IDPState_IDP_STATE_UNSPECIFIED
default:
return idp_pb.IDPState_IDP_STATE_UNSPECIFIED
}
}
func providerTypeToPb(idpType domain.IDPType) idp_pb.ProviderType {
switch idpType {
case domain.IDPTypeOIDC:
return idp_pb.ProviderType_PROVIDER_TYPE_OIDC
case domain.IDPTypeJWT:
return idp_pb.ProviderType_PROVIDER_TYPE_JWT
case domain.IDPTypeOAuth:
return idp_pb.ProviderType_PROVIDER_TYPE_OAUTH
case domain.IDPTypeLDAP:
return idp_pb.ProviderType_PROVIDER_TYPE_LDAP
case domain.IDPTypeAzureAD:
return idp_pb.ProviderType_PROVIDER_TYPE_AZURE_AD
case domain.IDPTypeGitHub:
return idp_pb.ProviderType_PROVIDER_TYPE_GITHUB
case domain.IDPTypeGitHubEE:
return idp_pb.ProviderType_PROVIDER_TYPE_GITHUB_EE
case domain.IDPTypeGitLab:
return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB
case domain.IDPTypeGitLabSelfHosted:
return idp_pb.ProviderType_PROVIDER_TYPE_GITLAB_SELF_HOSTED
case domain.IDPTypeGoogle:
return idp_pb.ProviderType_PROVIDER_TYPE_GOOGLE
case domain.IDPTypeUnspecified:
return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
default:
return idp_pb.ProviderType_PROVIDER_TYPE_UNSPECIFIED
}
}
func configToPb(config *query.IDPTemplate) *idp_pb.ProviderConfig {
providerConfig := &idp_pb.ProviderConfig{
Options: &idp_pb.Options{
IsLinkingAllowed: config.IsLinkingAllowed,
IsCreationAllowed: config.IsCreationAllowed,
IsAutoCreation: config.IsAutoCreation,
IsAutoUpdate: config.IsAutoUpdate,
},
}
if config.LDAPIDPTemplate != nil {
providerConfig.Config = &idp_pb.ProviderConfig_Ldap{
Ldap: &idp_pb.LDAPConfig{
Host: config.Host,
Port: config.Port,
Tls: config.TLS,
BaseDn: config.BaseDN,
UserObjectClass: config.UserObjectClass,
UserUniqueAttribute: config.UserUniqueAttribute,
Admin: config.Admin,
Attributes: ldapAttributesToPb(config.LDAPAttributes),
},
}
}
return providerConfig
}
func ldapAttributesToPb(attributes idp.LDAPAttributes) *idp_pb.LDAPAttributes {
return &idp_pb.LDAPAttributes{
IdAttribute: attributes.IDAttribute,
FirstNameAttribute: attributes.FirstNameAttribute,
LastNameAttribute: attributes.LastNameAttribute,
DisplayNameAttribute: attributes.DisplayNameAttribute,
NickNameAttribute: attributes.NickNameAttribute,
PreferredUsernameAttribute: attributes.PreferredUsernameAttribute,
EmailAttribute: attributes.EmailAttribute,
EmailVerifiedAttribute: attributes.EmailVerifiedAttribute,
PhoneAttribute: attributes.PhoneAttribute,
PhoneVerifiedAttribute: attributes.PhoneVerifiedAttribute,
PreferredLanguageAttribute: attributes.PreferredLanguageAttribute,
AvatarUrlAttribute: attributes.AvatarURLAttribute,
ProfileAttribute: attributes.ProfileAttribute,
}
}

View File

@@ -142,3 +142,57 @@ func (s *Server) UpdateOrgIDPJWTConfig(ctx context.Context, req *mgmt_pb.UpdateO
),
}, nil
}
func (s *Server) GetProviderByID(ctx context.Context, req *mgmt_pb.GetProviderByIDRequest) (*mgmt_pb.GetProviderByIDResponse, error) {
idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetCtxData(ctx).OrgID, false)
if err != nil {
return nil, err
}
return &mgmt_pb.GetProviderByIDResponse{Idp: idp_grpc.ProviderToPb(idp)}, nil
}
func (s *Server) ListProviders(ctx context.Context, req *mgmt_pb.ListProvidersRequest) (*mgmt_pb.ListProvidersResponse, error) {
queries, err := listProvidersToQuery(ctx, req)
if err != nil {
return nil, err
}
resp, err := s.query.IDPTemplates(ctx, queries, false)
if err != nil {
return nil, err
}
return &mgmt_pb.ListProvidersResponse{
Result: idp_grpc.ProvidersToPb(resp.Templates),
Details: object_pb.ToListDetails(resp.Count, resp.Sequence, resp.Timestamp),
}, nil
}
func (s *Server) AddLDAPProvider(ctx context.Context, req *mgmt_pb.AddLDAPProviderRequest) (*mgmt_pb.AddLDAPProviderResponse, error) {
id, details, err := s.command.AddOrgLDAPProvider(ctx, authz.GetCtxData(ctx).OrgID, addLDAPProviderToCommand(req))
if err != nil {
return nil, err
}
return &mgmt_pb.AddLDAPProviderResponse{
Id: id,
Details: object_pb.DomainToAddDetailsPb(details),
}, nil
}
func (s *Server) UpdateLDAPProvider(ctx context.Context, req *mgmt_pb.UpdateLDAPProviderRequest) (*mgmt_pb.UpdateLDAPProviderResponse, error) {
details, err := s.command.UpdateOrgLDAPProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id, updateLDAPProviderToCommand(req))
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateLDAPProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) DeleteProvider(ctx context.Context, req *mgmt_pb.DeleteProviderRequest) (*mgmt_pb.DeleteProviderResponse, error) {
details, err := s.command.DeleteOrgProvider(ctx, authz.GetCtxData(ctx).OrgID, req.Id)
if err != nil {
return nil, err
}
return &mgmt_pb.DeleteProviderResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -170,3 +171,81 @@ func userLinksToDomain(idps []*query.IDPUserLink) []*domain.UserIDPLink {
}
return links
}
func listProvidersToQuery(ctx context.Context, req *mgmt_pb.ListProvidersRequest) (*query.IDPTemplateSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := providerQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
resourceOwnerQuery, err := query.NewIDPTemplateResourceOwnerListSearchQuery(authz.GetInstance(ctx).InstanceID(), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
queries = append(queries, resourceOwnerQuery)
return &query.IDPTemplateSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func providerQueriesToQuery(queries []*mgmt_pb.ProviderQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = providerQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func providerQueryToQuery(idpQuery *mgmt_pb.ProviderQuery) (query.SearchQuery, error) {
switch q := idpQuery.Query.(type) {
case *mgmt_pb.ProviderQuery_IdpNameQuery:
return query.NewIDPTemplateNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
case *mgmt_pb.ProviderQuery_IdpIdQuery:
return query.NewIDPTemplateIDSearchQuery(q.IdpIdQuery.Id)
case *mgmt_pb.ProviderQuery_OwnerTypeQuery:
return query.NewIDPTemplateOwnerTypeSearchQuery(idp_grpc.IDPProviderTypeFromPb(q.OwnerTypeQuery.OwnerType))
default:
return nil, errors.ThrowInvalidArgument(nil, "ORG-Dr2aa", "List.Query.Invalid")
}
}
func addLDAPProviderToCommand(req *mgmt_pb.AddLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Host: req.Host,
Port: req.Port,
TLS: req.Tls,
BaseDN: req.BaseDn,
UserObjectClass: req.UserObjectClass,
UserUniqueAttribute: req.UserUniqueAttribute,
Admin: req.Admin,
Password: req.Password,
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Host: req.Host,
Port: req.Port,
TLS: req.Tls,
BaseDN: req.BaseDn,
UserObjectClass: req.UserObjectClass,
UserUniqueAttribute: req.UserUniqueAttribute,
Admin: req.Admin,
Password: req.Password,
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}

17
internal/command/idp.go Normal file
View File

@@ -0,0 +1,17 @@
package command
import "github.com/zitadel/zitadel/internal/repository/idp"
type LDAPProvider struct {
Name string
Host string
Port string
TLS bool
BaseDN string
UserObjectClass string
UserUniqueAttribute string
Admin string
Password string
LDAPAttributes idp.LDAPAttributes
IDPOptions idp.Options
}

View File

@@ -0,0 +1,208 @@
package command
import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
)
type LDAPIDPWriteModel struct {
eventstore.WriteModel
ID string
Name string
Host string
Port string
TLS bool
BaseDN string
UserObjectClass string
UserUniqueAttribute string
Admin string
Password *crypto.CryptoValue
idp.LDAPAttributes
idp.Options
State domain.IDPState
}
func (wm *LDAPIDPWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *idp.LDAPIDPAddedEvent:
if wm.ID != e.ID {
continue
}
wm.reduceAddeddEvent(e)
case *idp.LDAPIDPChangedEvent:
if wm.ID != e.ID {
continue
}
wm.reduceChangedEvent(e)
case *idp.RemovedEvent:
if wm.ID != e.ID {
continue
}
wm.State = domain.IDPStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *LDAPIDPWriteModel) reduceAddeddEvent(e *idp.LDAPIDPAddedEvent) {
wm.Name = e.Name
wm.Host = e.Host
wm.Port = e.Port
wm.TLS = e.TLS
wm.BaseDN = e.BaseDN
wm.UserObjectClass = e.UserObjectClass
wm.UserUniqueAttribute = e.UserUniqueAttribute
wm.Admin = e.Admin
wm.Password = e.Password
wm.LDAPAttributes = e.LDAPAttributes
wm.Options = e.Options
wm.State = domain.IDPStateActive
}
func (wm *LDAPIDPWriteModel) reduceChangedEvent(e *idp.LDAPIDPChangedEvent) {
if e.Name != nil {
wm.Name = *e.Name
}
if e.Name != nil {
wm.Name = *e.Name
}
if e.Host != nil {
wm.Host = *e.Host
}
if e.Port != nil {
wm.Port = *e.Port
}
if e.TLS != nil {
wm.TLS = *e.TLS
}
if e.BaseDN != nil {
wm.BaseDN = *e.BaseDN
}
if e.UserObjectClass != nil {
wm.UserObjectClass = *e.UserObjectClass
}
if e.UserUniqueAttribute != nil {
wm.UserUniqueAttribute = *e.UserUniqueAttribute
}
if e.Admin != nil {
wm.Admin = *e.Admin
}
if e.Password != nil {
wm.Password = e.Password
}
wm.LDAPAttributes.ReduceChanges(e.LDAPAttributeChanges)
wm.Options.ReduceChanges(e.OptionChanges)
}
func (wm *LDAPIDPWriteModel) NewChanges(
name,
host,
port string,
tls bool,
baseDN,
userObjectClass,
userUniqueAttribute,
admin string,
password string,
secretCrypto crypto.Crypto,
attributes idp.LDAPAttributes,
options idp.Options,
) ([]idp.LDAPIDPChanges, error) {
changes := make([]idp.LDAPIDPChanges, 0)
var cryptedPassword *crypto.CryptoValue
var err error
if password != "" {
cryptedPassword, err = crypto.Crypt([]byte(password), secretCrypto)
if err != nil {
return nil, err
}
changes = append(changes, idp.ChangeLDAPPassword(cryptedPassword))
}
if wm.Name != name {
changes = append(changes, idp.ChangeLDAPName(name))
}
if wm.Host != host {
changes = append(changes, idp.ChangeLDAPHost(host))
}
if wm.Port != port {
changes = append(changes, idp.ChangeLDAPPort(port))
}
if wm.TLS != tls {
changes = append(changes, idp.ChangeLDAPTLS(tls))
}
if wm.BaseDN != baseDN {
changes = append(changes, idp.ChangeLDAPBaseDN(baseDN))
}
if wm.UserObjectClass != userObjectClass {
changes = append(changes, idp.ChangeLDAPUserObjectClass(userObjectClass))
}
if wm.UserUniqueAttribute != userUniqueAttribute {
changes = append(changes, idp.ChangeLDAPUserUniqueAttribute(userUniqueAttribute))
}
if wm.Admin != admin {
changes = append(changes, idp.ChangeLDAPAdmin(admin))
}
attrs := wm.LDAPAttributes.Changes(attributes)
if !attrs.IsZero() {
changes = append(changes, idp.ChangeLDAPAttributes(attrs))
}
opts := wm.Options.Changes(options)
if !opts.IsZero() {
changes = append(changes, idp.ChangeLDAPOptions(opts))
}
return changes, nil
}
type IDPRemoveWriteModel struct {
eventstore.WriteModel
ID string
State domain.IDPState
name string
}
func (wm *IDPRemoveWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *idp.LDAPIDPAddedEvent:
wm.reduceAdded(e.ID, e.Name)
case *idp.LDAPIDPChangedEvent:
wm.reduceChanged(e.ID, e.Name)
case *idp.RemovedEvent:
wm.reduceRemoved(e.ID)
case *idpconfig.IDPConfigAddedEvent:
wm.reduceAdded(e.ConfigID, "")
case *idpconfig.IDPConfigRemovedEvent:
wm.reduceRemoved(e.ConfigID)
}
}
return wm.WriteModel.Reduce()
}
func (wm *IDPRemoveWriteModel) reduceAdded(id string, name string) {
if wm.ID != id {
return
}
wm.State = domain.IDPStateActive
wm.name = name
}
func (wm *IDPRemoveWriteModel) reduceChanged(id string, name *string) {
if wm.ID != id || name == nil {
return
}
wm.name = *name
}
func (wm *IDPRemoveWriteModel) reduceRemoved(id string) {
if wm.ID != id {
return
}
wm.State = domain.IDPStateRemoved
}

View File

@@ -0,0 +1,205 @@
package command
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"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/instance"
)
func (c *Commands) AddInstanceLDAPProvider(ctx context.Context, provider LDAPProvider) (string, *domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddInstanceLDAPProvider(instanceAgg, id, provider))
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) UpdateInstanceLDAPProvider(ctx context.Context, id string, provider LDAPProvider) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateInstanceLDAPProvider(instanceAgg, id, provider))
if err != nil {
return nil, err
}
if len(cmds) == 0 {
// no change, so return directly
return &domain.ObjectDetails{}, nil
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteInstanceProvider(instanceAgg, id))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) prepareAddInstanceLDAPProvider(a *instance.Aggregate, id string, provider LDAPProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAfdd", "Errors.Invalid.Argument")
}
if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SDVg2", "Errors.Invalid.Argument")
}
if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sv31s", "Errors.Invalid.Argument")
}
if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdgf4", "Errors.Invalid.Argument")
}
if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-AEG2w", "Errors.Invalid.Argument")
}
if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-SAD5n", "Errors.Invalid.Argument")
}
if provider.Password = strings.TrimSpace(provider.Password); provider.Password == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdf5h", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewLDAPInstanceIDPWriteModel(a.InstanceID, id)
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.Password), c.idpConfigEncryption)
if err != nil {
return nil, err
}
return []eventstore.Command{
instance.NewLDAPIDPAddedEvent(
ctx,
&a.Aggregate,
id,
provider.Name,
provider.Host,
provider.Port,
provider.TLS,
provider.BaseDN,
provider.UserObjectClass,
provider.UserUniqueAttribute,
provider.Admin,
secret,
provider.LDAPAttributes,
provider.IDPOptions,
),
}, nil
}, nil
}
}
func (c *Commands) prepareUpdateInstanceLDAPProvider(a *instance.Aggregate, id string, provider LDAPProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if id = strings.TrimSpace(id); id == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dgdbs", "Errors.Invalid.Argument")
}
if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Sffgd", "Errors.Invalid.Argument")
}
if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Dz62d", "Errors.Invalid.Argument")
}
if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-vb3ss", "Errors.Invalid.Argument")
}
if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-hbere", "Errors.Invalid.Argument")
}
if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-ASFt6", "Errors.Invalid.Argument")
}
if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-DG45z", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewLDAPInstanceIDPWriteModel(a.InstanceID, id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if !writeModel.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "INST-ASF3F", "Errors.Instance.IDPConfig.NotExisting")
}
event, err := writeModel.NewChangedEvent(
ctx,
&a.Aggregate,
id,
writeModel.Name,
provider.Name,
provider.Host,
provider.Port,
provider.TLS,
provider.BaseDN,
provider.UserObjectClass,
provider.UserUniqueAttribute,
provider.Admin,
provider.Password,
c.idpConfigEncryption,
provider.LDAPAttributes,
provider.IDPOptions,
)
if err != nil {
return nil, err
}
if event == nil {
return nil, nil
}
return []eventstore.Command{event}, nil
}, nil
}
}
func (c *Commands) prepareDeleteInstanceProvider(a *instance.Aggregate, id string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewInstanceIDPRemoveWriteModel(a.InstanceID, id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if !writeModel.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "INST-Se3tg", "Errors.Instance.IDPConfig.NotExisting")
}
return []eventstore.Command{instance.NewIDPRemovedEvent(ctx, &a.Aggregate, id, writeModel.name)}, nil
}, nil
}
}

View File

@@ -0,0 +1,163 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/instance"
)
type InstanceLDAPIDPWriteModel struct {
LDAPIDPWriteModel
}
func NewLDAPInstanceIDPWriteModel(instanceID, id string) *InstanceLDAPIDPWriteModel {
return &InstanceLDAPIDPWriteModel{
LDAPIDPWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: instanceID,
ResourceOwner: instanceID,
},
ID: id,
},
}
}
func (wm *InstanceLDAPIDPWriteModel) Reduce() error {
return wm.LDAPIDPWriteModel.Reduce()
}
func (wm *InstanceLDAPIDPWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *instance.LDAPIDPAddedEvent:
if wm.ID != e.ID {
continue
}
wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *instance.LDAPIDPChangedEvent:
if wm.ID != e.ID {
continue
}
wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
case *instance.IDPRemovedEvent:
if wm.ID != e.ID {
continue
}
wm.LDAPIDPWriteModel.AppendEvents(&e.RemovedEvent)
default:
wm.LDAPIDPWriteModel.AppendEvents(e)
}
}
}
func (wm *InstanceLDAPIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(instance.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
instance.LDAPIDPAddedEventType,
instance.LDAPIDPChangedEventType,
instance.IDPRemovedEventType,
).
Builder()
}
func (wm *InstanceLDAPIDPWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
oldName,
name,
host,
port string,
tls bool,
baseDN,
userObjectClass,
userUniqueAttribute,
admin string,
password string,
secretCrypto crypto.Crypto,
attributes idp.LDAPAttributes,
options idp.Options,
) (*instance.LDAPIDPChangedEvent, error) {
changes, err := wm.LDAPIDPWriteModel.NewChanges(
name,
host,
port,
tls,
baseDN,
userObjectClass,
userUniqueAttribute,
admin,
password,
secretCrypto,
attributes,
options,
)
if err != nil {
return nil, err
}
if len(changes) == 0 {
return nil, nil
}
changeEvent, err := instance.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes)
if err != nil {
return nil, err
}
return changeEvent, nil
}
type InstanceIDPRemoveWriteModel struct {
IDPRemoveWriteModel
}
func NewInstanceIDPRemoveWriteModel(instanceID, id string) *InstanceIDPRemoveWriteModel {
return &InstanceIDPRemoveWriteModel{
IDPRemoveWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: instanceID,
ResourceOwner: instanceID,
},
ID: id,
},
}
}
func (wm *InstanceIDPRemoveWriteModel) Reduce() error {
return wm.IDPRemoveWriteModel.Reduce()
}
func (wm *InstanceIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *instance.LDAPIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *instance.LDAPIDPChangedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
case *instance.IDPRemovedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
default:
wm.IDPRemoveWriteModel.AppendEvents(e)
}
}
}
func (wm *InstanceIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(instance.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
instance.LDAPIDPAddedEventType,
instance.LDAPIDPChangedEventType,
instance.IDPRemovedEventType,
).
Builder()
}

View File

@@ -0,0 +1,684 @@
package command
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
"github.com/zitadel/zitadel/internal/repository/instance"
)
func TestCommandSide_AddInstanceLDAPIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
provider LDAPProvider
}
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: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid host",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid baseDN",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
Host: "host",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userObjectClass",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userUniqueAttribute",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid admin",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid password",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
name: "ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"name",
"host",
"",
false,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{},
idp.Options{},
)),
},
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "instance1")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
Password: "password",
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
{
name: "ok all set",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"name",
"host",
"port",
true,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{
IDAttribute: "id",
FirstNameAttribute: "firstName",
LastNameAttribute: "lastName",
DisplayNameAttribute: "displayName",
NickNameAttribute: "nickName",
PreferredUsernameAttribute: "preferredUsername",
EmailAttribute: "email",
EmailVerifiedAttribute: "emailVerified",
PhoneAttribute: "phone",
PhoneVerifiedAttribute: "phoneVerified",
PreferredLanguageAttribute: "preferredLanguage",
AvatarURLAttribute: "avatarURL",
ProfileAttribute: "profile",
},
idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
)),
},
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "instance1")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{
Name: "name",
Host: "host",
Port: "port",
TLS: true,
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
Password: "password",
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: "id",
FirstNameAttribute: "firstName",
LastNameAttribute: "lastName",
DisplayNameAttribute: "displayName",
NickNameAttribute: "nickName",
PreferredUsernameAttribute: "preferredUsername",
EmailAttribute: "email",
EmailVerifiedAttribute: "emailVerified",
PhoneAttribute: "phone",
PhoneVerifiedAttribute: "phoneVerified",
PreferredLanguageAttribute: "preferredLanguage",
AvatarURLAttribute: "avatarURL",
ProfileAttribute: "profile",
},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
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,
idGenerator: tt.fields.idGenerator,
idpConfigEncryption: tt.fields.secretCrypto,
}
id, got, err := c.AddInstanceLDAPProvider(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, id)
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_UpdateInstanceLDAPIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
id string
provider LDAPProvider
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid id",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: LDAPProvider{},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid name",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid host",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "name",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid baseDN",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userObjectClass",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userUniqueAttribute",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid admin",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
name: "not found",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
},
},
res: res{
err: caos_errors.IsNotFound,
},
},
{
name: "no changes",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"name",
"host",
"",
false,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{},
idp.Options{},
)),
),
),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
},
},
res: res{
want: &domain.ObjectDetails{},
},
},
{
name: "change ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
instance.NewLDAPIDPAddedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"name",
"host",
"port",
false,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{},
idp.Options{},
)),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"instance1",
func() eventstore.Command {
t := true
event, _ := instance.NewLDAPIDPChangedEvent(context.Background(), &instance.NewAggregate("instance1").Aggregate,
"id1",
"name",
[]idp.LDAPIDPChanges{
idp.ChangeLDAPName("new name"),
idp.ChangeLDAPHost("new host"),
idp.ChangeLDAPPort("new port"),
idp.ChangeLDAPTLS(true),
idp.ChangeLDAPBaseDN("new baseDN"),
idp.ChangeLDAPUserObjectClass("new userObjectClass"),
idp.ChangeLDAPUserUniqueAttribute("new userUniqueAttribute"),
idp.ChangeLDAPAdmin("new admin"),
idp.ChangeLDAPPassword(&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("new password"),
}),
idp.ChangeLDAPAttributes(idp.LDAPAttributeChanges{
IDAttribute: stringPointer("new id"),
FirstNameAttribute: stringPointer("new firstName"),
LastNameAttribute: stringPointer("new lastName"),
DisplayNameAttribute: stringPointer("new displayName"),
NickNameAttribute: stringPointer("new nickName"),
PreferredUsernameAttribute: stringPointer("new preferredUsername"),
EmailAttribute: stringPointer("new email"),
EmailVerifiedAttribute: stringPointer("new emailVerified"),
PhoneAttribute: stringPointer("new phone"),
PhoneVerifiedAttribute: stringPointer("new phoneVerified"),
PreferredLanguageAttribute: stringPointer("new preferredLanguage"),
AvatarURLAttribute: stringPointer("new avatarURL"),
ProfileAttribute: stringPointer("new profile"),
}),
idp.ChangeLDAPOptions(idp.OptionChanges{
IsCreationAllowed: &t,
IsLinkingAllowed: &t,
IsAutoCreation: &t,
IsAutoUpdate: &t,
}),
},
)
return event
}(),
),
},
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name", "instance1")),
uniqueConstraintsFromEventConstraintWithInstanceID("instance1", idpconfig.NewAddIDPConfigNameUniqueConstraint("new name", "instance1")),
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: LDAPProvider{
Name: "new name",
Host: "new host",
Port: "new port",
TLS: true,
BaseDN: "new baseDN",
UserObjectClass: "new userObjectClass",
UserUniqueAttribute: "new userUniqueAttribute",
Admin: "new admin",
Password: "new password",
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: "new id",
FirstNameAttribute: "new firstName",
LastNameAttribute: "new lastName",
DisplayNameAttribute: "new displayName",
NickNameAttribute: "new nickName",
PreferredUsernameAttribute: "new preferredUsername",
EmailAttribute: "new email",
EmailVerifiedAttribute: "new emailVerified",
PhoneAttribute: "new phone",
PhoneVerifiedAttribute: "new phoneVerified",
PreferredLanguageAttribute: "new preferredLanguage",
AvatarURLAttribute: "new avatarURL",
ProfileAttribute: "new profile",
},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
res: res{
want: &domain.ObjectDetails{ResourceOwner: "instance1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idpConfigEncryption: tt.fields.secretCrypto,
}
got, err := c.UpdateInstanceLDAPProvider(tt.args.ctx, tt.args.id, 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.want, got)
}
})
}
}

204
internal/command/org_idp.go Normal file
View File

@@ -0,0 +1,204 @@
package command
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"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/org"
)
func (c *Commands) AddOrgLDAPProvider(ctx context.Context, resourceOwner string, provider LDAPProvider) (string, *domain.ObjectDetails, error) {
orgAgg := org.NewAggregate(resourceOwner)
id, err := c.idGenerator.Next()
if err != nil {
return "", nil, err
}
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgLDAPProvider(orgAgg, resourceOwner, id, provider))
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) UpdateOrgLDAPProvider(ctx context.Context, resourceOwner, id string, provider LDAPProvider) (*domain.ObjectDetails, error) {
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareUpdateOrgLDAPProvider(orgAgg, resourceOwner, id, provider))
if err != nil {
return nil, err
}
if len(cmds) == 0 {
// no change, so return directly
return &domain.ObjectDetails{}, nil
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) DeleteOrgProvider(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareDeleteOrgProvider(orgAgg, resourceOwner, id))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) prepareAddOrgLDAPProvider(a *org.Aggregate, resourceOwner, id string, provider LDAPProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAfdd", "Errors.Invalid.Argument")
}
if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SDVg2", "Errors.Invalid.Argument")
}
if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sv31s", "Errors.Invalid.Argument")
}
if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdgf4", "Errors.Invalid.Argument")
}
if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-AEG2w", "Errors.Invalid.Argument")
}
if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAD5n", "Errors.Invalid.Argument")
}
if provider.Password = strings.TrimSpace(provider.Password); provider.Password == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sdf5h", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id)
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.Password), c.idpConfigEncryption)
if err != nil {
return nil, err
}
return []eventstore.Command{
org.NewLDAPIDPAddedEvent(
ctx,
&a.Aggregate,
id,
provider.Name,
provider.Host,
provider.Port,
provider.TLS,
provider.BaseDN,
provider.UserObjectClass,
provider.UserUniqueAttribute,
provider.Admin,
secret,
provider.LDAPAttributes,
provider.IDPOptions,
),
}, nil
}, nil
}
}
func (c *Commands) prepareUpdateOrgLDAPProvider(a *org.Aggregate, resourceOwner, id string, provider LDAPProvider) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if id = strings.TrimSpace(id); id == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dgdbs", "Errors.Invalid.Argument")
}
if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Sffgd", "Errors.Invalid.Argument")
}
if provider.Host = strings.TrimSpace(provider.Host); provider.Host == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Dz62d", "Errors.Invalid.Argument")
}
if provider.BaseDN = strings.TrimSpace(provider.BaseDN); provider.BaseDN == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-vb3ss", "Errors.Invalid.Argument")
}
if provider.UserObjectClass = strings.TrimSpace(provider.UserObjectClass); provider.UserObjectClass == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-hbere", "Errors.Invalid.Argument")
}
if provider.UserUniqueAttribute = strings.TrimSpace(provider.UserUniqueAttribute); provider.UserUniqueAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-ASFt6", "Errors.Invalid.Argument")
}
if provider.Admin = strings.TrimSpace(provider.Admin); provider.Admin == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-DG45z", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewLDAPOrgIDPWriteModel(resourceOwner, id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if !writeModel.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "ORG-ASF3F", "Errors.Org.IDPConfig.NotExisting")
}
event, err := writeModel.NewChangedEvent(
ctx,
&a.Aggregate,
id,
writeModel.Name,
provider.Name,
provider.Host,
provider.Port,
provider.TLS,
provider.BaseDN,
provider.UserObjectClass,
provider.UserUniqueAttribute,
provider.Admin,
provider.Password,
c.idpConfigEncryption,
provider.LDAPAttributes,
provider.IDPOptions,
)
if err != nil {
return nil, err
}
if event == nil {
return nil, nil
}
return []eventstore.Command{event}, nil
}, nil
}
}
func (c *Commands) prepareDeleteOrgProvider(a *org.Aggregate, resourceOwner, id string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewOrgIDPRemoveWriteModel(resourceOwner, id)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if !writeModel.State.Exists() {
return nil, caos_errs.ThrowNotFound(nil, "ORG-Se3tg", "Errors.Org.IDPConfig.NotExisting")
}
return []eventstore.Command{org.NewIDPRemovedEvent(ctx, &a.Aggregate, id, writeModel.name)}, nil
}, nil
}
}

View File

@@ -0,0 +1,163 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/org"
)
type OrgLDAPIDPWriteModel struct {
LDAPIDPWriteModel
}
func NewLDAPOrgIDPWriteModel(orgID, id string) *OrgLDAPIDPWriteModel {
return &OrgLDAPIDPWriteModel{
LDAPIDPWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
ID: id,
},
}
}
func (wm *OrgLDAPIDPWriteModel) Reduce() error {
return wm.LDAPIDPWriteModel.Reduce()
}
func (wm *OrgLDAPIDPWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *org.LDAPIDPAddedEvent:
if wm.ID != e.ID {
continue
}
wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *org.LDAPIDPChangedEvent:
if wm.ID != e.ID {
continue
}
wm.LDAPIDPWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
case *org.IDPRemovedEvent:
if wm.ID != e.ID {
continue
}
wm.LDAPIDPWriteModel.AppendEvents(&e.RemovedEvent)
default:
wm.LDAPIDPWriteModel.AppendEvents(e)
}
}
}
func (wm *OrgLDAPIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(org.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
org.LDAPIDPAddedEventType,
org.LDAPIDPChangedEventType,
org.IDPRemovedEventType,
).
Builder()
}
func (wm *OrgLDAPIDPWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
oldName,
name,
host,
port string,
tls bool,
baseDN,
userObjectClass,
userUniqueAttribute,
admin string,
password string,
secretCrypto crypto.Crypto,
attributes idp.LDAPAttributes,
options idp.Options,
) (*org.LDAPIDPChangedEvent, error) {
changes, err := wm.LDAPIDPWriteModel.NewChanges(
name,
host,
port,
tls,
baseDN,
userObjectClass,
userUniqueAttribute,
admin,
password,
secretCrypto,
attributes,
options,
)
if err != nil {
return nil, err
}
if len(changes) == 0 {
return nil, nil
}
changeEvent, err := org.NewLDAPIDPChangedEvent(ctx, aggregate, id, oldName, changes)
if err != nil {
return nil, err
}
return changeEvent, nil
}
type OrgIDPRemoveWriteModel struct {
IDPRemoveWriteModel
}
func NewOrgIDPRemoveWriteModel(orgID, id string) *OrgIDPRemoveWriteModel {
return &OrgIDPRemoveWriteModel{
IDPRemoveWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
ID: id,
},
}
}
func (wm *OrgIDPRemoveWriteModel) Reduce() error {
return wm.IDPRemoveWriteModel.Reduce()
}
func (wm *OrgIDPRemoveWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *org.LDAPIDPAddedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPAddedEvent)
case *org.LDAPIDPChangedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.LDAPIDPChangedEvent)
case *org.IDPRemovedEvent:
wm.IDPRemoveWriteModel.AppendEvents(&e.RemovedEvent)
default:
wm.IDPRemoveWriteModel.AppendEvents(e)
}
}
}
func (wm *OrgIDPRemoveWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(org.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
org.LDAPIDPAddedEventType,
org.LDAPIDPChangedEventType,
org.IDPRemovedEventType,
).
Builder()
}

View File

@@ -0,0 +1,698 @@
package command
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
"github.com/zitadel/zitadel/internal/repository/org"
)
func TestCommandSide_AddOrgLDAPIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
resourceOwner string
provider LDAPProvider
}
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: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid host",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid baseDN",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
Host: "host",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userObjectClass",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userUniqueAttribute",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid admin",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid password",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
name: "ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
eventPusherToEvents(
org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"id1",
"name",
"host",
"",
false,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{},
idp.Options{},
)),
uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "org1")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
Password: "password",
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "org1"},
},
},
{
name: "ok all set",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
eventPusherToEvents(
org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"id1",
"name",
"host",
"port",
true,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{
IDAttribute: "id",
FirstNameAttribute: "firstName",
LastNameAttribute: "lastName",
DisplayNameAttribute: "displayName",
NickNameAttribute: "nickName",
PreferredUsernameAttribute: "preferredUsername",
EmailAttribute: "email",
EmailVerifiedAttribute: "emailVerified",
PhoneAttribute: "phone",
PhoneVerifiedAttribute: "phoneVerified",
PreferredLanguageAttribute: "preferredLanguage",
AvatarURLAttribute: "avatarURL",
ProfileAttribute: "profile",
},
idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
)),
uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("name", "org1")),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{
Name: "name",
Host: "host",
Port: "port",
TLS: true,
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
Password: "password",
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: "id",
FirstNameAttribute: "firstName",
LastNameAttribute: "lastName",
DisplayNameAttribute: "displayName",
NickNameAttribute: "nickName",
PreferredUsernameAttribute: "preferredUsername",
EmailAttribute: "email",
EmailVerifiedAttribute: "emailVerified",
PhoneAttribute: "phone",
PhoneVerifiedAttribute: "phoneVerified",
PreferredLanguageAttribute: "preferredLanguage",
AvatarURLAttribute: "avatarURL",
ProfileAttribute: "profile",
},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
res: res{
id: "id1",
want: &domain.ObjectDetails{ResourceOwner: "org1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
idpConfigEncryption: tt.fields.secretCrypto,
}
id, got, err := c.AddOrgLDAPProvider(tt.args.ctx, tt.args.resourceOwner, 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, id)
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_UpdateOrgLDAPIDP(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
secretCrypto crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
resourceOwner string
id string
provider LDAPProvider
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"invalid id",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: LDAPProvider{},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid name",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid host",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "name",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid baseDN",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userObjectClass",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid userUniqueAttribute",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
"invalid admin",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{
name: "not found",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
},
},
res: res{
err: caos_errors.IsNotFound,
},
},
{
name: "no changes",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"id1",
"name",
"host",
"",
false,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{},
idp.Options{},
)),
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "name",
Host: "host",
BaseDN: "baseDN",
UserObjectClass: "userObjectClass",
UserUniqueAttribute: "userUniqueAttribute",
Admin: "admin",
},
},
res: res{
want: &domain.ObjectDetails{},
},
},
{
name: "change ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
org.NewLDAPIDPAddedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"id1",
"name",
"host",
"port",
false,
"baseDN",
"userObjectClass",
"userUniqueAttribute",
"admin",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("password"),
},
idp.LDAPAttributes{},
idp.Options{},
)),
),
expectPush(
eventPusherToEvents(
func() eventstore.Command {
t := true
event, _ := org.NewLDAPIDPChangedEvent(context.Background(), &org.NewAggregate("org1").Aggregate,
"id1",
"name",
[]idp.LDAPIDPChanges{
idp.ChangeLDAPName("new name"),
idp.ChangeLDAPHost("new host"),
idp.ChangeLDAPPort("new port"),
idp.ChangeLDAPTLS(true),
idp.ChangeLDAPBaseDN("new baseDN"),
idp.ChangeLDAPUserObjectClass("new userObjectClass"),
idp.ChangeLDAPUserUniqueAttribute("new userUniqueAttribute"),
idp.ChangeLDAPAdmin("new admin"),
idp.ChangeLDAPPassword(&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("new password"),
}),
idp.ChangeLDAPAttributes(idp.LDAPAttributeChanges{
IDAttribute: stringPointer("new id"),
FirstNameAttribute: stringPointer("new firstName"),
LastNameAttribute: stringPointer("new lastName"),
DisplayNameAttribute: stringPointer("new displayName"),
NickNameAttribute: stringPointer("new nickName"),
PreferredUsernameAttribute: stringPointer("new preferredUsername"),
EmailAttribute: stringPointer("new email"),
EmailVerifiedAttribute: stringPointer("new emailVerified"),
PhoneAttribute: stringPointer("new phone"),
PhoneVerifiedAttribute: stringPointer("new phoneVerified"),
PreferredLanguageAttribute: stringPointer("new preferredLanguage"),
AvatarURLAttribute: stringPointer("new avatarURL"),
ProfileAttribute: stringPointer("new profile"),
}),
idp.ChangeLDAPOptions(idp.OptionChanges{
IsCreationAllowed: &t,
IsLinkingAllowed: &t,
IsAutoCreation: &t,
IsAutoUpdate: &t,
}),
},
)
return event
}(),
),
uniqueConstraintsFromEventConstraint(idpconfig.NewRemoveIDPConfigNameUniqueConstraint("name", "org1")),
uniqueConstraintsFromEventConstraint(idpconfig.NewAddIDPConfigNameUniqueConstraint("new name", "org1")),
),
),
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: LDAPProvider{
Name: "new name",
Host: "new host",
Port: "new port",
TLS: true,
BaseDN: "new baseDN",
UserObjectClass: "new userObjectClass",
UserUniqueAttribute: "new userUniqueAttribute",
Admin: "new admin",
Password: "new password",
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: "new id",
FirstNameAttribute: "new firstName",
LastNameAttribute: "new lastName",
DisplayNameAttribute: "new displayName",
NickNameAttribute: "new nickName",
PreferredUsernameAttribute: "new preferredUsername",
EmailAttribute: "new email",
EmailVerifiedAttribute: "new emailVerified",
PhoneAttribute: "new phone",
PhoneVerifiedAttribute: "new phoneVerified",
PreferredLanguageAttribute: "new preferredLanguage",
AvatarURLAttribute: "new avatarURL",
ProfileAttribute: "new profile",
},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
res: res{
want: &domain.ObjectDetails{ResourceOwner: "org1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idpConfigEncryption: tt.fields.secretCrypto,
}
got, err := c.UpdateOrgLDAPProvider(tt.args.ctx, tt.args.resourceOwner, tt.args.id, 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.want, got)
}
})
}
}
func stringPointer(s string) *string {
return &s
}

36
internal/domain/idp.go Normal file
View File

@@ -0,0 +1,36 @@
package domain
type IDPState int32
const (
IDPStateUnspecified IDPState = iota
IDPStateActive
IDPStateInactive
IDPStateRemoved
idpStateCount
)
func (s IDPState) Valid() bool {
return s >= 0 && s < idpStateCount
}
func (s IDPState) Exists() bool {
return s != IDPStateUnspecified && s != IDPStateRemoved
}
type IDPType int32
const (
IDPTypeUnspecified IDPType = iota
IDPTypeOIDC
IDPTypeJWT
IDPTypeOAuth
IDPTypeLDAP
IDPTypeAzureAD
IDPTypeGitHub
IDPTypeGitHubEE
IDPTypeGitLab
IDPTypeGitLabSelfHosted
IDPTypeGoogle
)

View File

@@ -0,0 +1,237 @@
package ldap
import (
"context"
"github.com/zitadel/zitadel/internal/idp"
)
const DefaultPort = "389"
var _ idp.Provider = (*Provider)(nil)
// Provider is the [idp.Provider] implementation for a generic LDAP provider
type Provider struct {
name string
host string
port string
tls bool
baseDN string
userObjectClass string
userUniqueAttribute string
admin string
password string
loginUrl string
isLinkingAllowed bool
isCreationAllowed bool
isAutoCreation bool
isAutoUpdate bool
idAttribute string
firstNameAttribute string
lastNameAttribute string
displayNameAttribute string
nickNameAttribute string
preferredUsernameAttribute string
emailAttribute string
emailVerifiedAttribute string
phoneAttribute string
phoneVerifiedAttribute string
preferredLanguageAttribute string
avatarURLAttribute string
profileAttribute string
}
type ProviderOpts func(provider *Provider)
// WithLinkingAllowed allows end users to link the federated user to an existing one.
func WithLinkingAllowed() ProviderOpts {
return func(p *Provider) {
p.isLinkingAllowed = true
}
}
// WithCreationAllowed allows end users to create a new user using the federated information.
func WithCreationAllowed() ProviderOpts {
return func(p *Provider) {
p.isCreationAllowed = true
}
}
// WithAutoCreation enables that federated users are automatically created if not already existing.
func WithAutoCreation() ProviderOpts {
return func(p *Provider) {
p.isAutoCreation = true
}
}
// WithAutoUpdate enables that information retrieved from the provider is automatically used to update
// the existing user on each authentication.
func WithAutoUpdate() ProviderOpts {
return func(p *Provider) {
p.isAutoUpdate = true
}
}
// WithCustomPort configures a custom port used for the communication instead of :389 as per default
func WithCustomPort(port string) ProviderOpts {
return func(p *Provider) {
p.port = port
}
}
// Insecure configures to communication insecure with the LDAP server without TLS
func Insecure() ProviderOpts {
return func(p *Provider) {
p.tls = false
}
}
// WithCustomIDAttribute configures to map the LDAP attribute to the user, default is the uniqueUserAttribute
func WithCustomIDAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.idAttribute = name
}
}
// WithFirstNameAttribute configures to map the LDAP attribute to the user
func WithFirstNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.firstNameAttribute = name
}
}
// WithLastNameAttribute configures to map the LDAP attribute to the user
func WithLastNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.lastNameAttribute = name
}
}
// WithDisplayNameAttribute configures to map the LDAP attribute to the user
func WithDisplayNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.displayNameAttribute = name
}
}
// WithNickNameAttribute configures to map the LDAP attribute to the user
func WithNickNameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.nickNameAttribute = name
}
}
// WithPreferredUsernameAttribute configures to map the LDAP attribute to the user
func WithPreferredUsernameAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.preferredUsernameAttribute = name
}
}
// WithEmailAttribute configures to map the LDAP attribute to the user
func WithEmailAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.emailAttribute = name
}
}
// WithEmailVerifiedAttribute configures to map the LDAP attribute to the user
func WithEmailVerifiedAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.emailVerifiedAttribute = name
}
}
// WithPhoneAttribute configures to map the LDAP attribute to the user
func WithPhoneAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.phoneAttribute = name
}
}
// WithPhoneVerifiedAttribute configures to map the LDAP attribute to the user
func WithPhoneVerifiedAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.phoneVerifiedAttribute = name
}
}
// WithPreferredLanguageAttribute configures to map the LDAP attribute to the user
func WithPreferredLanguageAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.preferredLanguageAttribute = name
}
}
// WithAvatarURLAttribute configures to map the LDAP attribute to the user
func WithAvatarURLAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.avatarURLAttribute = name
}
}
// WithProfileAttribute configures to map the LDAP attribute to the user
func WithProfileAttribute(name string) ProviderOpts {
return func(p *Provider) {
p.profileAttribute = name
}
}
func New(
name string,
host string,
baseDN string,
userObjectClass string,
userUniqueAttribute string,
admin string,
password string,
loginUrl string,
options ...ProviderOpts,
) *Provider {
provider := &Provider{
name: name,
host: host,
port: DefaultPort,
tls: true,
baseDN: baseDN,
userObjectClass: userObjectClass,
userUniqueAttribute: userUniqueAttribute,
admin: admin,
password: password,
loginUrl: loginUrl,
idAttribute: userUniqueAttribute,
}
for _, option := range options {
option(provider)
}
return provider
}
func (p *Provider) Name() string {
return p.name
}
func (p *Provider) BeginAuth(ctx context.Context, state string, params ...any) (idp.Session, error) {
return &Session{
Provider: p,
loginUrl: p.loginUrl + "?state=" + state,
}, nil
}
func (p *Provider) IsLinkingAllowed() bool {
return p.isLinkingAllowed
}
func (p *Provider) IsCreationAllowed() bool {
return p.isCreationAllowed
}
func (p *Provider) IsAutoCreation() bool {
return p.isAutoCreation
}
func (p *Provider) IsAutoUpdate() bool {
return p.isAutoUpdate
}

View File

@@ -0,0 +1,185 @@
package ldap
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestProvider_Options(t *testing.T) {
type fields struct {
name string
host string
baseDN string
userObjectClass string
userUniqueAttribute string
admin string
password string
loginUrl string
opts []ProviderOpts
}
type want struct {
name string
port string
tls bool
linkingAllowed bool
creationAllowed bool
autoCreation bool
autoUpdate bool
idAttribute string
firstNameAttribute string
lastNameAttribute string
displayNameAttribute string
nickNameAttribute string
preferredUsernameAttribute string
emailAttribute string
emailVerifiedAttribute string
phoneAttribute string
phoneVerifiedAttribute string
preferredLanguageAttribute string
avatarURLAttribute string
profileAttribute string
}
tests := []struct {
name string
fields fields
want want
}{
{
name: "default",
fields: fields{
name: "ldap",
host: "host",
baseDN: "base",
userObjectClass: "class",
userUniqueAttribute: "attr",
admin: "admin",
password: "password",
loginUrl: "url",
opts: nil,
},
want: want{
name: "ldap",
port: DefaultPort,
tls: true,
linkingAllowed: false,
creationAllowed: false,
autoCreation: false,
autoUpdate: false,
idAttribute: "attr",
},
},
{
name: "all true",
fields: fields{
name: "ldap",
host: "host",
baseDN: "base",
userObjectClass: "class",
userUniqueAttribute: "attr",
admin: "admin",
password: "password",
loginUrl: "url",
opts: []ProviderOpts{
WithLinkingAllowed(),
WithCreationAllowed(),
WithAutoCreation(),
WithAutoUpdate(),
},
},
want: want{
name: "ldap",
port: DefaultPort,
tls: true,
linkingAllowed: true,
creationAllowed: true,
autoCreation: true,
autoUpdate: true,
idAttribute: "attr",
},
}, {
name: "all true, attributes set",
fields: fields{
name: "ldap",
host: "host",
baseDN: "base",
userObjectClass: "class",
userUniqueAttribute: "attr",
admin: "admin",
password: "password",
loginUrl: "url",
opts: []ProviderOpts{
Insecure(),
WithCustomPort("port"),
WithLinkingAllowed(),
WithCreationAllowed(),
WithAutoCreation(),
WithAutoUpdate(),
WithCustomIDAttribute("id"),
WithFirstNameAttribute("first"),
WithLastNameAttribute("last"),
WithDisplayNameAttribute("display"),
WithNickNameAttribute("nick"),
WithPreferredUsernameAttribute("prefUser"),
WithEmailAttribute("email"),
WithEmailVerifiedAttribute("emailVerified"),
WithPhoneAttribute("phone"),
WithPhoneVerifiedAttribute("phoneVerified"),
WithPreferredLanguageAttribute("prefLang"),
WithAvatarURLAttribute("avatar"),
WithProfileAttribute("profile"),
},
},
want: want{
name: "ldap",
port: "port",
tls: false,
linkingAllowed: true,
creationAllowed: true,
autoCreation: true,
autoUpdate: true,
idAttribute: "id",
firstNameAttribute: "first",
lastNameAttribute: "last",
displayNameAttribute: "display",
nickNameAttribute: "nick",
preferredUsernameAttribute: "prefUser",
emailAttribute: "email",
emailVerifiedAttribute: "emailVerified",
phoneAttribute: "phone",
phoneVerifiedAttribute: "phoneVerified",
preferredLanguageAttribute: "prefLang",
avatarURLAttribute: "avatar",
profileAttribute: "profile",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
provider := New(tt.fields.name, tt.fields.host, tt.fields.baseDN, tt.fields.userObjectClass, tt.fields.userUniqueAttribute, tt.fields.admin, tt.fields.password, tt.fields.loginUrl, tt.fields.opts...)
a.Equal(tt.want.name, provider.Name())
a.Equal(tt.want.port, provider.port)
a.Equal(tt.want.tls, provider.tls)
a.Equal(tt.want.linkingAllowed, provider.IsLinkingAllowed())
a.Equal(tt.want.creationAllowed, provider.IsCreationAllowed())
a.Equal(tt.want.autoCreation, provider.IsAutoCreation())
a.Equal(tt.want.autoUpdate, provider.IsAutoUpdate())
a.Equal(tt.want.idAttribute, provider.idAttribute)
a.Equal(tt.want.firstNameAttribute, provider.firstNameAttribute)
a.Equal(tt.want.lastNameAttribute, provider.lastNameAttribute)
a.Equal(tt.want.displayNameAttribute, provider.displayNameAttribute)
a.Equal(tt.want.nickNameAttribute, provider.nickNameAttribute)
a.Equal(tt.want.preferredUsernameAttribute, provider.preferredUsernameAttribute)
a.Equal(tt.want.emailAttribute, provider.emailAttribute)
a.Equal(tt.want.emailVerifiedAttribute, provider.emailVerifiedAttribute)
a.Equal(tt.want.phoneAttribute, provider.phoneAttribute)
a.Equal(tt.want.phoneVerifiedAttribute, provider.phoneVerifiedAttribute)
a.Equal(tt.want.preferredLanguageAttribute, provider.preferredLanguageAttribute)
a.Equal(tt.want.avatarURLAttribute, provider.avatarURLAttribute)
a.Equal(tt.want.profileAttribute, provider.profileAttribute)
})
}
}

View File

@@ -0,0 +1,98 @@
package ldap
import (
"context"
"crypto/tls"
"errors"
"fmt"
"strconv"
"github.com/go-ldap/ldap/v3"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/idp"
)
var ErrNoSingleUser = errors.New("user does not exist or too many entries returned")
var _ idp.Session = (*Session)(nil)
type Session struct {
Provider *Provider
loginUrl string
user string
password string
}
func (s *Session) GetAuthURL() string {
return s.loginUrl
}
func (s *Session) FetchUser(_ context.Context) (idp.User, error) {
l, err := ldap.DialURL("ldap://" + s.Provider.host + ":" + s.Provider.port)
if err != nil {
return nil, err
}
defer l.Close()
if s.Provider.tls {
err = l.StartTLS(&tls.Config{ServerName: s.Provider.host})
if err != nil {
return nil, err
}
}
// Bind as the admin to search for user
err = l.Bind("cn="+s.Provider.admin+","+s.Provider.baseDN, s.Provider.password)
if err != nil {
return nil, err
}
// Search for user with the unique attribute for the userDN
searchRequest := ldap.NewSearchRequest(
s.Provider.baseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass="+s.Provider.userObjectClass+")("+s.Provider.userUniqueAttribute+"=%s))", ldap.EscapeFilter(s.user)),
[]string{"dn"},
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return nil, err
}
if len(sr.Entries) != 1 {
return nil, ErrNoSingleUser
}
user := sr.Entries[0]
// Bind as the user to verify their password
err = l.Bind(user.DN, s.password)
if err != nil {
return nil, err
}
emailVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.emailVerifiedAttribute))
if err != nil {
return nil, err
}
phoneVerified, err := strconv.ParseBool(user.GetAttributeValue(s.Provider.phoneVerifiedAttribute))
if err != nil {
return nil, err
}
return NewUser(
user.GetAttributeValue(s.Provider.idAttribute),
user.GetAttributeValue(s.Provider.firstNameAttribute),
user.GetAttributeValue(s.Provider.lastNameAttribute),
user.GetAttributeValue(s.Provider.displayNameAttribute),
user.GetAttributeValue(s.Provider.nickNameAttribute),
user.GetAttributeValue(s.Provider.preferredUsernameAttribute),
user.GetAttributeValue(s.Provider.emailAttribute),
emailVerified,
user.GetAttributeValue(s.Provider.phoneAttribute),
phoneVerified,
language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)),
user.GetAttributeValue(s.Provider.avatarURLAttribute),
user.GetAttributeValue(s.Provider.profileAttribute),
), nil
}

View File

@@ -0,0 +1,91 @@
package ldap
import "golang.org/x/text/language"
type User struct {
id string
firstName string
lastName string
displayName string
nickName string
preferredUsername string
email string
emailVerified bool
phone string
phoneVerified bool
preferredLanguage language.Tag
avatarURL string
profile string
}
func NewUser(
id string,
firstName string,
lastName string,
displayName string,
nickName string,
preferredUsername string,
email string,
emailVerified bool,
phone string,
phoneVerified bool,
preferredLanguage language.Tag,
avatarURL string,
profile string,
) *User {
return &User{
id,
firstName,
lastName,
displayName,
nickName,
preferredUsername,
email,
emailVerified,
phone,
phoneVerified,
preferredLanguage,
avatarURL,
profile,
}
}
func (u *User) GetID() string {
return u.id
}
func (u *User) GetFirstName() string {
return u.firstName
}
func (u *User) GetLastName() string {
return u.lastName
}
func (u *User) GetDisplayName() string {
return u.displayName
}
func (u *User) GetNickname() string {
return u.nickName
}
func (u *User) GetPreferredUsername() string {
return u.preferredUsername
}
func (u *User) GetEmail() string {
return u.email
}
func (u *User) IsEmailVerified() bool {
return u.emailVerified
}
func (u *User) GetPhone() string {
return u.phone
}
func (u *User) IsPhoneVerified() bool {
return u.phoneVerified
}
func (u *User) GetPreferredLanguage() language.Tag {
return u.preferredLanguage
}
func (u *User) GetAvatarURL() string {
return u.avatarURL
}
func (u *User) GetProfile() string {
return u.profile
}

View File

@@ -0,0 +1,621 @@
package query
import (
"context"
"database/sql"
errs "errors"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
type IDPTemplate struct {
CreationDate time.Time
ChangeDate time.Time
Sequence uint64
ResourceOwner string
ID string
State domain.IDPState
Name string
Type domain.IDPType
OwnerType domain.IdentityProviderType
IsCreationAllowed bool
IsLinkingAllowed bool
IsAutoCreation bool
IsAutoUpdate bool
*LDAPIDPTemplate
}
type IDPTemplates struct {
SearchResponse
Templates []*IDPTemplate
}
type LDAPIDPTemplate struct {
IDPID string
Host string
Port string
TLS bool
BaseDN string
UserObjectClass string
UserUniqueAttribute string
Admin string
Password *crypto.CryptoValue
idp.LDAPAttributes
idp.Options
}
var (
idpTemplateTable = table{
name: projection.IDPTemplateTable,
instanceIDCol: projection.IDPTemplateInstanceIDCol,
}
IDPTemplateIDCol = Column{
name: projection.IDPTemplateIDCol,
table: idpTemplateTable,
}
IDPTemplateCreationDateCol = Column{
name: projection.IDPTemplateCreationDateCol,
table: idpTemplateTable,
}
IDPTemplateChangeDateCol = Column{
name: projection.IDPTemplateChangeDateCol,
table: idpTemplateTable,
}
IDPTemplateSequenceCol = Column{
name: projection.IDPTemplateSequenceCol,
table: idpTemplateTable,
}
IDPTemplateResourceOwnerCol = Column{
name: projection.IDPTemplateResourceOwnerCol,
table: idpTemplateTable,
}
IDPTemplateInstanceIDCol = Column{
name: projection.IDPTemplateInstanceIDCol,
table: idpTemplateTable,
}
IDPTemplateStateCol = Column{
name: projection.IDPTemplateStateCol,
table: idpTemplateTable,
}
IDPTemplateNameCol = Column{
name: projection.IDPTemplateNameCol,
table: idpTemplateTable,
}
IDPTemplateOwnerTypeCol = Column{
name: projection.IDPOwnerTypeCol,
table: idpTemplateTable,
}
IDPTemplateTypeCol = Column{
name: projection.IDPTemplateTypeCol,
table: idpTemplateTable,
}
IDPTemplateOwnerRemovedCol = Column{
name: projection.IDPTemplateOwnerRemovedCol,
table: idpTemplateTable,
}
IDPTemplateIsCreationAllowedCol = Column{
name: projection.IDPTemplateIsCreationAllowedCol,
table: idpTemplateTable,
}
IDPTemplateIsLinkingAllowedCol = Column{
name: projection.IDPTemplateIsLinkingAllowedCol,
table: idpTemplateTable,
}
IDPTemplateIsAutoCreationCol = Column{
name: projection.IDPTemplateIsAutoCreationCol,
table: idpTemplateTable,
}
IDPTemplateIsAutoUpdateCol = Column{
name: projection.IDPTemplateIsAutoUpdateCol,
table: idpTemplateTable,
}
)
var (
ldapIdpTemplateTable = table{
name: projection.IDPTemplateLDAPTable,
instanceIDCol: projection.IDPTemplateInstanceIDCol,
}
LDAPIDCol = Column{
name: projection.LDAPIDCol,
table: ldapIdpTemplateTable,
}
LDAPInstanceIDCol = Column{
name: projection.LDAPInstanceIDCol,
table: ldapIdpTemplateTable,
}
LDAPHostCol = Column{
name: projection.LDAPHostCol,
table: ldapIdpTemplateTable,
}
LDAPPortCol = Column{
name: projection.LDAPPortCol,
table: ldapIdpTemplateTable,
}
LDAPTlsCol = Column{
name: projection.LDAPTlsCol,
table: ldapIdpTemplateTable,
}
LDAPBaseDNCol = Column{
name: projection.LDAPBaseDNCol,
table: ldapIdpTemplateTable,
}
LDAPUserObjectClassCol = Column{
name: projection.LDAPUserObjectClassCol,
table: ldapIdpTemplateTable,
}
LDAPUserUniqueAttributeCol = Column{
name: projection.LDAPUserUniqueAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPAdminCol = Column{
name: projection.LDAPAdminCol,
table: ldapIdpTemplateTable,
}
LDAPPasswordCol = Column{
name: projection.LDAPPasswordCol,
table: ldapIdpTemplateTable,
}
LDAPIDAttributeCol = Column{
name: projection.LDAPIDAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPFirstNameAttributeCol = Column{
name: projection.LDAPFirstNameAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPLastNameAttributeCol = Column{
name: projection.LDAPLastNameAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPDisplayNameAttributeCol = Column{
name: projection.LDAPDisplayNameAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPNickNameAttributeCol = Column{
name: projection.LDAPNickNameAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPPreferredUsernameAttributeCol = Column{
name: projection.LDAPPreferredUsernameAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPEmailAttributeCol = Column{
name: projection.LDAPEmailAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPEmailVerifiedAttributeCol = Column{
name: projection.LDAPEmailVerifiedAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPPhoneAttributeCol = Column{
name: projection.LDAPPhoneAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPPhoneVerifiedAttributeCol = Column{
name: projection.LDAPPhoneVerifiedAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPPreferredLanguageAttributeCol = Column{
name: projection.LDAPPreferredLanguageAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPAvatarURLAttributeCol = Column{
name: projection.LDAPAvatarURLAttributeCol,
table: ldapIdpTemplateTable,
}
LDAPProfileAttributeCol = Column{
name: projection.LDAPProfileAttributeCol,
table: ldapIdpTemplateTable,
}
)
// IDPTemplateByIDAndResourceOwner searches for the requested id in the context of the resource owner and IAM
func (q *Queries) IDPTemplateByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk bool, id, resourceOwner string, withOwnerRemoved bool) (_ *IDPTemplate, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
err := projection.IDPTemplateProjection.Trigger(ctx)
logging.OnError(err).WithField("projection", idpTemplateTable.identifier()).Warn("could not trigger projection for query")
}
eq := sq.Eq{
IDPTemplateIDCol.identifier(): id,
IDPTemplateInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
if !withOwnerRemoved {
eq[IDPTemplateOwnerRemovedCol.identifier()] = false
}
where := sq.And{
eq,
sq.Or{
sq.Eq{IDPTemplateResourceOwnerCol.identifier(): resourceOwner},
sq.Eq{IDPTemplateResourceOwnerCol.identifier(): authz.GetInstance(ctx).InstanceID()},
},
}
stmt, scan := prepareIDPTemplateByIDQuery()
query, args, err := stmt.Where(where).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-SFAew", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
// IDPTemplates searches idp templates matching the query
func (q *Queries) IDPTemplates(ctx context.Context, queries *IDPTemplateSearchQueries, withOwnerRemoved bool) (idps *IDPTemplates, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareIDPTemplatesQuery()
eq := sq.Eq{
IDPTemplateInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
if !withOwnerRemoved {
eq[IDPTemplateOwnerRemovedCol.identifier()] = false
}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-SAF34", "Errors.Query.InvalidRequest")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-BDFrq", "Errors.Internal")
}
idps, err = scan(rows)
if err != nil {
return nil, err
}
idps.LatestSequence, err = q.latestSequence(ctx, idpTemplateTable)
return idps, err
}
type IDPTemplateSearchQueries struct {
SearchRequest
Queries []SearchQuery
}
func NewIDPTemplateIDSearchQuery(id string) (SearchQuery, error) {
return NewTextQuery(IDPTemplateIDCol, id, TextEquals)
}
func NewIDPTemplateOwnerTypeSearchQuery(ownerType domain.IdentityProviderType) (SearchQuery, error) {
return NewNumberQuery(IDPTemplateOwnerTypeCol, ownerType, NumberEquals)
}
func NewIDPTemplateNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
return NewTextQuery(IDPTemplateNameCol, value, method)
}
func NewIDPTemplateResourceOwnerSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(IDPTemplateResourceOwnerCol, value, TextEquals)
}
func NewIDPTemplateResourceOwnerListSearchQuery(ids ...string) (SearchQuery, error) {
list := make([]interface{}, len(ids))
for i, value := range ids {
list[i] = value
}
return NewListQuery(IDPTemplateResourceOwnerCol, list, ListIn)
}
func (q *IDPTemplateSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = q.SearchRequest.toQuery(query)
for _, q := range q.Queries {
query = q.toQuery(query)
}
return query
}
func prepareIDPTemplateByIDQuery() (sq.SelectBuilder, func(*sql.Row) (*IDPTemplate, error)) {
return sq.Select(
IDPTemplateIDCol.identifier(),
IDPTemplateResourceOwnerCol.identifier(),
IDPTemplateCreationDateCol.identifier(),
IDPTemplateChangeDateCol.identifier(),
IDPTemplateSequenceCol.identifier(),
IDPTemplateStateCol.identifier(),
IDPTemplateNameCol.identifier(),
IDPTemplateTypeCol.identifier(),
IDPTemplateOwnerTypeCol.identifier(),
IDPTemplateIsCreationAllowedCol.identifier(),
IDPTemplateIsLinkingAllowedCol.identifier(),
IDPTemplateIsAutoCreationCol.identifier(),
IDPTemplateIsAutoUpdateCol.identifier(),
LDAPIDCol.identifier(),
LDAPHostCol.identifier(),
LDAPPortCol.identifier(),
LDAPTlsCol.identifier(),
LDAPBaseDNCol.identifier(),
LDAPUserObjectClassCol.identifier(),
LDAPUserUniqueAttributeCol.identifier(),
LDAPAdminCol.identifier(),
LDAPPasswordCol.identifier(),
LDAPIDAttributeCol.identifier(),
LDAPFirstNameAttributeCol.identifier(),
LDAPLastNameAttributeCol.identifier(),
LDAPDisplayNameAttributeCol.identifier(),
LDAPNickNameAttributeCol.identifier(),
LDAPPreferredUsernameAttributeCol.identifier(),
LDAPEmailAttributeCol.identifier(),
LDAPEmailVerifiedAttributeCol.identifier(),
LDAPPhoneAttributeCol.identifier(),
LDAPPhoneVerifiedAttributeCol.identifier(),
LDAPPreferredLanguageAttributeCol.identifier(),
LDAPAvatarURLAttributeCol.identifier(),
LDAPProfileAttributeCol.identifier(),
).From(idpTemplateTable.identifier()).
LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*IDPTemplate, error) {
idpTemplate := new(IDPTemplate)
ldapID := sql.NullString{}
ldapHost := sql.NullString{}
ldapPort := sql.NullString{}
ldapTls := sql.NullBool{}
ldapBaseDN := sql.NullString{}
ldapUserObjectClass := sql.NullString{}
ldapUserUniqueAttribute := sql.NullString{}
ldapAdmin := sql.NullString{}
ldapPassword := new(crypto.CryptoValue)
ldapIDAttribute := sql.NullString{}
ldapFirstNameAttribute := sql.NullString{}
ldapLastNameAttribute := sql.NullString{}
ldapDisplayNameAttribute := sql.NullString{}
ldapNickNameAttribute := sql.NullString{}
ldapPreferredUsernameAttribute := sql.NullString{}
ldapEmailAttribute := sql.NullString{}
ldapEmailVerifiedAttribute := sql.NullString{}
ldapPhoneAttribute := sql.NullString{}
ldapPhoneVerifiedAttribute := sql.NullString{}
ldapPreferredLanguageAttribute := sql.NullString{}
ldapAvatarURLAttribute := sql.NullString{}
ldapProfileAttribute := sql.NullString{}
err := row.Scan(
&idpTemplate.ID,
&idpTemplate.ResourceOwner,
&idpTemplate.CreationDate,
&idpTemplate.ChangeDate,
&idpTemplate.Sequence,
&idpTemplate.State,
&idpTemplate.Name,
&idpTemplate.Type,
&idpTemplate.OwnerType,
&idpTemplate.IsCreationAllowed,
&idpTemplate.IsLinkingAllowed,
&idpTemplate.IsAutoCreation,
&idpTemplate.IsAutoUpdate,
&ldapID,
&ldapHost,
&ldapPort,
&ldapTls,
&ldapBaseDN,
&ldapUserObjectClass,
&ldapUserUniqueAttribute,
&ldapAdmin,
&ldapPassword,
&ldapIDAttribute,
&ldapFirstNameAttribute,
&ldapLastNameAttribute,
&ldapDisplayNameAttribute,
&ldapNickNameAttribute,
&ldapPreferredUsernameAttribute,
&ldapEmailAttribute,
&ldapEmailVerifiedAttribute,
&ldapPhoneAttribute,
&ldapPhoneVerifiedAttribute,
&ldapPreferredLanguageAttribute,
&ldapAvatarURLAttribute,
&ldapProfileAttribute,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-SAFrt", "Errors.IDPConfig.NotExisting")
}
return nil, errors.ThrowInternal(err, "QUERY-ADG42", "Errors.Internal")
}
if ldapID.Valid {
idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{
IDPID: ldapID.String,
Host: ldapHost.String,
Port: ldapPort.String,
TLS: ldapTls.Bool,
BaseDN: ldapBaseDN.String,
UserObjectClass: ldapUserObjectClass.String,
UserUniqueAttribute: ldapUserUniqueAttribute.String,
Admin: ldapAdmin.String,
Password: ldapPassword,
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: ldapIDAttribute.String,
FirstNameAttribute: ldapFirstNameAttribute.String,
LastNameAttribute: ldapLastNameAttribute.String,
DisplayNameAttribute: ldapDisplayNameAttribute.String,
NickNameAttribute: ldapNickNameAttribute.String,
PreferredUsernameAttribute: ldapPreferredUsernameAttribute.String,
EmailAttribute: ldapEmailAttribute.String,
EmailVerifiedAttribute: ldapEmailVerifiedAttribute.String,
PhoneAttribute: ldapPhoneAttribute.String,
PhoneVerifiedAttribute: ldapPhoneVerifiedAttribute.String,
PreferredLanguageAttribute: ldapPreferredLanguageAttribute.String,
AvatarURLAttribute: ldapAvatarURLAttribute.String,
ProfileAttribute: ldapProfileAttribute.String,
},
}
}
return idpTemplate, nil
}
}
func prepareIDPTemplatesQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPTemplates, error)) {
return sq.Select(
IDPTemplateIDCol.identifier(),
IDPTemplateResourceOwnerCol.identifier(),
IDPTemplateCreationDateCol.identifier(),
IDPTemplateChangeDateCol.identifier(),
IDPTemplateSequenceCol.identifier(),
IDPTemplateStateCol.identifier(),
IDPTemplateNameCol.identifier(),
IDPTemplateTypeCol.identifier(),
IDPTemplateOwnerTypeCol.identifier(),
IDPTemplateIsCreationAllowedCol.identifier(),
IDPTemplateIsLinkingAllowedCol.identifier(),
IDPTemplateIsAutoCreationCol.identifier(),
IDPTemplateIsAutoUpdateCol.identifier(),
LDAPIDCol.identifier(),
LDAPHostCol.identifier(),
LDAPPortCol.identifier(),
LDAPTlsCol.identifier(),
LDAPBaseDNCol.identifier(),
LDAPUserObjectClassCol.identifier(),
LDAPUserUniqueAttributeCol.identifier(),
LDAPAdminCol.identifier(),
LDAPPasswordCol.identifier(),
LDAPIDAttributeCol.identifier(),
LDAPFirstNameAttributeCol.identifier(),
LDAPLastNameAttributeCol.identifier(),
LDAPDisplayNameAttributeCol.identifier(),
LDAPNickNameAttributeCol.identifier(),
LDAPPreferredUsernameAttributeCol.identifier(),
LDAPEmailAttributeCol.identifier(),
LDAPEmailVerifiedAttributeCol.identifier(),
LDAPPhoneAttributeCol.identifier(),
LDAPPhoneVerifiedAttributeCol.identifier(),
LDAPPreferredLanguageAttributeCol.identifier(),
LDAPAvatarURLAttributeCol.identifier(),
LDAPProfileAttributeCol.identifier(),
countColumn.identifier(),
).From(idpTemplateTable.identifier()).
LeftJoin(join(LDAPIDCol, IDPTemplateIDCol)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*IDPTemplates, error) {
templates := make([]*IDPTemplate, 0)
var count uint64
for rows.Next() {
idpTemplate := new(IDPTemplate)
ldapID := sql.NullString{}
ldapHost := sql.NullString{}
ldapPort := sql.NullString{}
ldapTls := sql.NullBool{}
ldapBaseDN := sql.NullString{}
ldapUserObjectClass := sql.NullString{}
ldapUserUniqueAttribute := sql.NullString{}
ldapAdmin := sql.NullString{}
ldapPassword := new(crypto.CryptoValue)
ldapIDAttribute := sql.NullString{}
ldapFirstNameAttribute := sql.NullString{}
ldapLastNameAttribute := sql.NullString{}
ldapDisplayNameAttribute := sql.NullString{}
ldapNickNameAttribute := sql.NullString{}
ldapPreferredUsernameAttribute := sql.NullString{}
ldapEmailAttribute := sql.NullString{}
ldapEmailVerifiedAttribute := sql.NullString{}
ldapPhoneAttribute := sql.NullString{}
ldapPhoneVerifiedAttribute := sql.NullString{}
ldapPreferredLanguageAttribute := sql.NullString{}
ldapAvatarURLAttribute := sql.NullString{}
ldapProfileAttribute := sql.NullString{}
err := rows.Scan(
&idpTemplate.ID,
&idpTemplate.ResourceOwner,
&idpTemplate.CreationDate,
&idpTemplate.ChangeDate,
&idpTemplate.Sequence,
&idpTemplate.State,
&idpTemplate.Name,
&idpTemplate.Type,
&idpTemplate.OwnerType,
&idpTemplate.IsCreationAllowed,
&idpTemplate.IsLinkingAllowed,
&idpTemplate.IsAutoCreation,
&idpTemplate.IsAutoUpdate,
&ldapID,
&ldapHost,
&ldapPort,
&ldapTls,
&ldapBaseDN,
&ldapUserObjectClass,
&ldapUserUniqueAttribute,
&ldapAdmin,
&ldapPassword,
&ldapIDAttribute,
&ldapFirstNameAttribute,
&ldapLastNameAttribute,
&ldapDisplayNameAttribute,
&ldapNickNameAttribute,
&ldapPreferredUsernameAttribute,
&ldapEmailAttribute,
&ldapEmailVerifiedAttribute,
&ldapPhoneAttribute,
&ldapPhoneVerifiedAttribute,
&ldapPreferredLanguageAttribute,
&ldapAvatarURLAttribute,
&ldapProfileAttribute,
&count,
)
if err != nil {
return nil, err
}
if ldapID.Valid {
idpTemplate.LDAPIDPTemplate = &LDAPIDPTemplate{
IDPID: ldapID.String,
Host: ldapHost.String,
Port: ldapPort.String,
TLS: ldapTls.Bool,
BaseDN: ldapBaseDN.String,
UserObjectClass: ldapUserObjectClass.String,
UserUniqueAttribute: ldapUserUniqueAttribute.String,
Admin: ldapAdmin.String,
Password: ldapPassword,
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: ldapIDAttribute.String,
FirstNameAttribute: ldapFirstNameAttribute.String,
LastNameAttribute: ldapLastNameAttribute.String,
DisplayNameAttribute: ldapDisplayNameAttribute.String,
NickNameAttribute: ldapNickNameAttribute.String,
PreferredUsernameAttribute: ldapPreferredUsernameAttribute.String,
EmailAttribute: ldapEmailAttribute.String,
EmailVerifiedAttribute: ldapEmailVerifiedAttribute.String,
PhoneAttribute: ldapPhoneAttribute.String,
PhoneVerifiedAttribute: ldapPhoneVerifiedAttribute.String,
PreferredLanguageAttribute: ldapPreferredLanguageAttribute.String,
AvatarURLAttribute: ldapAvatarURLAttribute.String,
ProfileAttribute: ldapProfileAttribute.String,
},
}
}
templates = append(templates, idpTemplate)
}
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-SAGrt", "Errors.Query.CloseRows")
}
return &IDPTemplates{
Templates: templates,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}

View File

@@ -0,0 +1,727 @@
package query
import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"regexp"
"testing"
"github.com/zitadel/zitadel/internal/domain"
errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/repository/idp"
)
var (
idpTemplateQuery = `SELECT projections.idp_templates.id,` +
` projections.idp_templates.resource_owner,` +
` projections.idp_templates.creation_date,` +
` projections.idp_templates.change_date,` +
` projections.idp_templates.sequence,` +
` projections.idp_templates.state,` +
` projections.idp_templates.name,` +
` projections.idp_templates.type,` +
` projections.idp_templates.owner_type,` +
` projections.idp_templates.is_creation_allowed,` +
` projections.idp_templates.is_linking_allowed,` +
` projections.idp_templates.is_auto_creation,` +
` projections.idp_templates.is_auto_update,` +
` projections.idp_templates_ldap.idp_id,` +
` projections.idp_templates_ldap.host,` +
` projections.idp_templates_ldap.port,` +
` projections.idp_templates_ldap.tls,` +
` projections.idp_templates_ldap.base_dn,` +
` projections.idp_templates_ldap.user_object_class,` +
` projections.idp_templates_ldap.user_unique_attribute,` +
` projections.idp_templates_ldap.admin,` +
` projections.idp_templates_ldap.password,` +
` projections.idp_templates_ldap.id_attribute,` +
` projections.idp_templates_ldap.first_name_attribute,` +
` projections.idp_templates_ldap.last_name_attribute,` +
` projections.idp_templates_ldap.display_name_attribute,` +
` projections.idp_templates_ldap.nick_name_attribute,` +
` projections.idp_templates_ldap.preferred_username_attribute,` +
` projections.idp_templates_ldap.email_attribute,` +
` projections.idp_templates_ldap.email_verified,` +
` projections.idp_templates_ldap.phone_attribute,` +
` projections.idp_templates_ldap.phone_verified_attribute,` +
` projections.idp_templates_ldap.preferred_language_attribute,` +
` projections.idp_templates_ldap.avatar_url_attribute,` +
` projections.idp_templates_ldap.profile_attribute` +
` FROM projections.idp_templates` +
` LEFT JOIN projections.idp_templates_ldap ON projections.idp_templates.id = projections.idp_templates_ldap.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_ldap.instance_id`
idpTemplateCols = []string{
"id",
"resource_owner",
"creation_date",
"change_date",
"sequence",
"state",
"name",
"type",
"owner_type",
"is_creation_allowed",
"is_linking_allowed",
"is_auto_creation",
"is_auto_update",
// ldap config
"idp_id",
"host",
"port",
"tls",
"base_dn",
"user_object_class",
"user_unique_attribute",
"admin",
"password",
"id_attribute",
"first_name_attribute",
"last_name_attribute",
"display_name_attribute",
"nick_name_attribute",
"preferred_username_attribute",
"email_attribute",
"email_verified",
"phone_attribute",
"phone_verified_attribute",
"preferred_language_attribute",
"avatar_url_attribute",
"profile_attribute",
}
idpTemplatesQuery = `SELECT projections.idp_templates.id,` +
` projections.idp_templates.resource_owner,` +
` projections.idp_templates.creation_date,` +
` projections.idp_templates.change_date,` +
` projections.idp_templates.sequence,` +
` projections.idp_templates.state,` +
` projections.idp_templates.name,` +
` projections.idp_templates.type,` +
` projections.idp_templates.owner_type,` +
` projections.idp_templates.is_creation_allowed,` +
` projections.idp_templates.is_linking_allowed,` +
` projections.idp_templates.is_auto_creation,` +
` projections.idp_templates.is_auto_update,` +
` projections.idp_templates_ldap.idp_id,` +
` projections.idp_templates_ldap.host,` +
` projections.idp_templates_ldap.port,` +
` projections.idp_templates_ldap.tls,` +
` projections.idp_templates_ldap.base_dn,` +
` projections.idp_templates_ldap.user_object_class,` +
` projections.idp_templates_ldap.user_unique_attribute,` +
` projections.idp_templates_ldap.admin,` +
` projections.idp_templates_ldap.password,` +
` projections.idp_templates_ldap.id_attribute,` +
` projections.idp_templates_ldap.first_name_attribute,` +
` projections.idp_templates_ldap.last_name_attribute,` +
` projections.idp_templates_ldap.display_name_attribute,` +
` projections.idp_templates_ldap.nick_name_attribute,` +
` projections.idp_templates_ldap.preferred_username_attribute,` +
` projections.idp_templates_ldap.email_attribute,` +
` projections.idp_templates_ldap.email_verified,` +
` projections.idp_templates_ldap.phone_attribute,` +
` projections.idp_templates_ldap.phone_verified_attribute,` +
` projections.idp_templates_ldap.preferred_language_attribute,` +
` projections.idp_templates_ldap.avatar_url_attribute,` +
` projections.idp_templates_ldap.profile_attribute,` +
` COUNT(*) OVER ()` +
` FROM projections.idp_templates` +
` LEFT JOIN projections.idp_templates_ldap ON projections.idp_templates.id = projections.idp_templates_ldap.idp_id AND projections.idp_templates.instance_id = projections.idp_templates_ldap.instance_id`
idpTemplatesCols = []string{
"id",
"resource_owner",
"creation_date",
"change_date",
"sequence",
"state",
"name",
"type",
"owner_type",
"is_creation_allowed",
"is_linking_allowed",
"is_auto_creation",
"is_auto_update",
"idp_id",
"host",
"port",
"tls",
"base_dn",
"user_object_class",
"user_unique_attribute",
"admin",
"password",
"id_attribute",
"first_name_attribute",
"last_name_attribute",
"display_name_attribute",
"nick_name_attribute",
"preferred_username_attribute",
"email_attribute",
"email_verified",
"phone_attribute",
"phone_verified_attribute",
"preferred_language_attribute",
"avatar_url_attribute",
"profile_attribute",
"count",
}
)
func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
type want struct {
sqlExpectations sqlExpectation
err checkErr
}
tests := []struct {
name string
prepare interface{}
want want
object interface{}
}{
{
name: "prepareIDPTemplateByIDQuery no result",
prepare: prepareIDPTemplateByIDQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(idpTemplateQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !errs.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*IDPTemplate)(nil),
},
{
name: "prepareIDPTemplateByIDQuery ldap idp",
prepare: prepareIDPTemplateByIDQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(idpTemplateQuery),
idpTemplateCols,
[]driver.Value{
"idp-id",
"ro",
testNow,
testNow,
uint64(20211109),
domain.IDPConfigStateActive,
"idp-name",
domain.IDPTypeLDAP,
domain.IdentityProviderTypeOrg,
true,
true,
true,
true,
// ldap config
"idp-id",
"host",
"port",
true,
"base",
"user",
"uid",
"admin",
nil,
"id",
"first",
"last",
"display",
"nickname",
"username",
"email",
"emailVerified",
"phone",
"phoneVerified",
"lang",
"avatar",
"profile",
},
),
},
object: &IDPTemplate{
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
ID: "idp-id",
State: domain.IDPStateActive,
Name: "idp-name",
Type: domain.IDPTypeLDAP,
OwnerType: domain.IdentityProviderTypeOrg,
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
LDAPIDPTemplate: &LDAPIDPTemplate{
IDPID: "idp-id",
Host: "host",
Port: "port",
TLS: true,
BaseDN: "base",
UserObjectClass: "user",
UserUniqueAttribute: "uid",
Admin: "admin",
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: "id",
FirstNameAttribute: "first",
LastNameAttribute: "last",
DisplayNameAttribute: "display",
NickNameAttribute: "nickname",
PreferredUsernameAttribute: "username",
EmailAttribute: "email",
EmailVerifiedAttribute: "emailVerified",
PhoneAttribute: "phone",
PhoneVerifiedAttribute: "phoneVerified",
PreferredLanguageAttribute: "lang",
AvatarURLAttribute: "avatar",
ProfileAttribute: "profile",
},
},
},
},
{
name: "prepareIDPTemplateByIDQuery no config",
prepare: prepareIDPTemplateByIDQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(idpTemplateQuery),
idpTemplateCols,
[]driver.Value{
"idp-id",
"ro",
testNow,
testNow,
uint64(20211109),
domain.IDPConfigStateActive,
"idp-name",
domain.IDPTypeLDAP,
domain.IdentityProviderTypeOrg,
true,
true,
true,
true,
// ldap config
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
},
),
},
object: &IDPTemplate{
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
ID: "idp-id",
State: domain.IDPStateActive,
Name: "idp-name",
Type: domain.IDPTypeLDAP,
OwnerType: domain.IdentityProviderTypeOrg,
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
{
name: "prepareIDPTemplateByIDQuery sql err",
prepare: prepareIDPTemplateByIDQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(idpTemplateQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: nil,
},
{
name: "prepareIDPTemplatesQuery no result",
prepare: prepareIDPTemplatesQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(idpTemplatesQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !errs.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: &IDPTemplates{Templates: []*IDPTemplate{}},
},
{
name: "prepareIDPTemplatesQuery ldap idp",
prepare: prepareIDPTemplatesQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(idpTemplatesQuery),
idpTemplatesCols,
[][]driver.Value{
{
"idp-id",
"ro",
testNow,
testNow,
uint64(20211109),
domain.IDPConfigStateActive,
"idp-name",
domain.IDPTypeLDAP,
domain.IdentityProviderTypeOrg,
true,
true,
true,
true,
// ldap config
"idp-id",
"host",
"port",
true,
"base",
"user",
"uid",
"admin",
nil,
"id",
"first",
"last",
"display",
"nickname",
"username",
"email",
"emailVerified",
"phone",
"phoneVerified",
"lang",
"avatar",
"profile",
},
},
),
},
object: &IDPTemplates{
SearchResponse: SearchResponse{
Count: 1,
},
Templates: []*IDPTemplate{
{
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
ID: "idp-id",
State: domain.IDPStateActive,
Name: "idp-name",
Type: domain.IDPTypeLDAP,
OwnerType: domain.IdentityProviderTypeOrg,
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
LDAPIDPTemplate: &LDAPIDPTemplate{
IDPID: "idp-id",
Host: "host",
Port: "port",
TLS: true,
BaseDN: "base",
UserObjectClass: "user",
UserUniqueAttribute: "uid",
Admin: "admin",
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: "id",
FirstNameAttribute: "first",
LastNameAttribute: "last",
DisplayNameAttribute: "display",
NickNameAttribute: "nickname",
PreferredUsernameAttribute: "username",
EmailAttribute: "email",
EmailVerifiedAttribute: "emailVerified",
PhoneAttribute: "phone",
PhoneVerifiedAttribute: "phoneVerified",
PreferredLanguageAttribute: "lang",
AvatarURLAttribute: "avatar",
ProfileAttribute: "profile",
},
},
},
},
},
},
{
name: "prepareIDPTemplatesQuery no config",
prepare: prepareIDPTemplatesQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(idpTemplatesQuery),
idpTemplatesCols,
[][]driver.Value{
{
"idp-id",
"ro",
testNow,
testNow,
uint64(20211109),
domain.IDPConfigStateActive,
"idp-name",
domain.IDPTypeLDAP,
domain.IdentityProviderTypeOrg,
true,
true,
true,
true,
// ldap config
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
},
},
),
},
object: &IDPTemplates{
SearchResponse: SearchResponse{
Count: 1,
},
Templates: []*IDPTemplate{
{
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
ID: "idp-id",
State: domain.IDPStateActive,
Name: "idp-name",
Type: domain.IDPTypeLDAP,
OwnerType: domain.IdentityProviderTypeOrg,
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
},
{
name: "prepareIDPTemplatesQuery all config types",
prepare: prepareIDPTemplatesQuery,
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(idpTemplatesQuery),
idpTemplatesCols,
[][]driver.Value{
{
"idp-id-1",
"ro",
testNow,
testNow,
uint64(20211109),
domain.IDPConfigStateActive,
"idp-name",
domain.IDPTypeLDAP,
domain.IdentityProviderTypeOrg,
true,
true,
true,
true,
// ldap config
"idp-id",
"host",
"port",
true,
"base",
"user",
"uid",
"admin",
nil,
"id",
"first",
"last",
"display",
"nickname",
"username",
"email",
"emailVerified",
"phone",
"phoneVerified",
"lang",
"avatar",
"profile",
},
{
"idp-id-2",
"ro",
testNow,
testNow,
uint64(20211109),
domain.IDPConfigStateActive,
"idp-name",
domain.IDPTypeLDAP,
domain.IdentityProviderTypeOrg,
true,
true,
true,
true,
// ldap config
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
},
},
),
},
object: &IDPTemplates{
SearchResponse: SearchResponse{
Count: 2,
},
Templates: []*IDPTemplate{
{
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
ID: "idp-id-1",
State: domain.IDPStateActive,
Name: "idp-name",
Type: domain.IDPTypeLDAP,
OwnerType: domain.IdentityProviderTypeOrg,
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
LDAPIDPTemplate: &LDAPIDPTemplate{
IDPID: "idp-id",
Host: "host",
Port: "port",
TLS: true,
BaseDN: "base",
UserObjectClass: "user",
UserUniqueAttribute: "uid",
Admin: "admin",
LDAPAttributes: idp.LDAPAttributes{
IDAttribute: "id",
FirstNameAttribute: "first",
LastNameAttribute: "last",
DisplayNameAttribute: "display",
NickNameAttribute: "nickname",
PreferredUsernameAttribute: "username",
EmailAttribute: "email",
EmailVerifiedAttribute: "emailVerified",
PhoneAttribute: "phone",
PhoneVerifiedAttribute: "phoneVerified",
PreferredLanguageAttribute: "lang",
AvatarURLAttribute: "avatar",
ProfileAttribute: "profile",
},
},
},
{
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
ID: "idp-id-2",
State: domain.IDPStateActive,
Name: "idp-name",
Type: domain.IDPTypeLDAP,
OwnerType: domain.IdentityProviderTypeOrg,
IsCreationAllowed: true,
IsLinkingAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
},
},
},
},
{
name: "prepareIDPTemplatesQuery sql err",
prepare: prepareIDPTemplatesQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(idpTemplatesQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
})
}
}

View File

@@ -0,0 +1,412 @@
package projection
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
)
const (
IDPTemplateTable = "projections.idp_templates"
IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix
IDPTemplateLDAPSuffix = "ldap"
IDPTemplateIDCol = "id"
IDPTemplateCreationDateCol = "creation_date"
IDPTemplateChangeDateCol = "change_date"
IDPTemplateSequenceCol = "sequence"
IDPTemplateResourceOwnerCol = "resource_owner"
IDPTemplateInstanceIDCol = "instance_id"
IDPTemplateStateCol = "state"
IDPTemplateNameCol = "name"
IDPTemplateOwnerTypeCol = "owner_type"
IDPTemplateTypeCol = "type"
IDPTemplateOwnerRemovedCol = "owner_removed"
IDPTemplateIsCreationAllowedCol = "is_creation_allowed"
IDPTemplateIsLinkingAllowedCol = "is_linking_allowed"
IDPTemplateIsAutoCreationCol = "is_auto_creation"
IDPTemplateIsAutoUpdateCol = "is_auto_update"
LDAPIDCol = "idp_id"
LDAPInstanceIDCol = "instance_id"
LDAPHostCol = "host"
LDAPPortCol = "port"
LDAPTlsCol = "tls"
LDAPBaseDNCol = "base_dn"
LDAPUserObjectClassCol = "user_object_class"
LDAPUserUniqueAttributeCol = "user_unique_attribute"
LDAPAdminCol = "admin"
LDAPPasswordCol = "password"
LDAPIDAttributeCol = "id_attribute"
LDAPFirstNameAttributeCol = "first_name_attribute"
LDAPLastNameAttributeCol = "last_name_attribute"
LDAPDisplayNameAttributeCol = "display_name_attribute"
LDAPNickNameAttributeCol = "nick_name_attribute"
LDAPPreferredUsernameAttributeCol = "preferred_username_attribute"
LDAPEmailAttributeCol = "email_attribute"
LDAPEmailVerifiedAttributeCol = "email_verified"
LDAPPhoneAttributeCol = "phone_attribute"
LDAPPhoneVerifiedAttributeCol = "phone_verified_attribute"
LDAPPreferredLanguageAttributeCol = "preferred_language_attribute"
LDAPAvatarURLAttributeCol = "avatar_url_attribute"
LDAPProfileAttributeCol = "profile_attribute"
)
type idpTemplateProjection struct {
crdb.StatementHandler
}
func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerConfig) *idpTemplateProjection {
p := new(idpTemplateProjection)
config.ProjectionName = IDPTemplateTable
config.Reducers = p.reducers()
config.InitCheck = crdb.NewMultiTableCheck(
crdb.NewTable([]*crdb.Column{
crdb.NewColumn(IDPTemplateIDCol, crdb.ColumnTypeText),
crdb.NewColumn(IDPTemplateCreationDateCol, crdb.ColumnTypeTimestamp),
crdb.NewColumn(IDPTemplateChangeDateCol, crdb.ColumnTypeTimestamp),
crdb.NewColumn(IDPTemplateSequenceCol, crdb.ColumnTypeInt64),
crdb.NewColumn(IDPTemplateResourceOwnerCol, crdb.ColumnTypeText),
crdb.NewColumn(IDPTemplateInstanceIDCol, crdb.ColumnTypeText),
crdb.NewColumn(IDPTemplateStateCol, crdb.ColumnTypeEnum),
crdb.NewColumn(IDPTemplateNameCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(IDPTemplateOwnerTypeCol, crdb.ColumnTypeEnum),
crdb.NewColumn(IDPTemplateTypeCol, crdb.ColumnTypeEnum),
crdb.NewColumn(IDPTemplateOwnerRemovedCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(IDPTemplateIsCreationAllowedCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(IDPTemplateIsLinkingAllowedCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(IDPTemplateIsAutoCreationCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(IDPTemplateIsAutoUpdateCol, crdb.ColumnTypeBool, crdb.Default(false)),
},
crdb.NewPrimaryKey(IDPTemplateInstanceIDCol, IDPTemplateIDCol),
crdb.WithIndex(crdb.NewIndex("resource_owner", []string{IDPTemplateResourceOwnerCol})),
crdb.WithIndex(crdb.NewIndex("owner_removed", []string{IDPTemplateOwnerRemovedCol})),
),
crdb.NewSuffixedTable([]*crdb.Column{
crdb.NewColumn(LDAPIDCol, crdb.ColumnTypeText),
crdb.NewColumn(LDAPInstanceIDCol, crdb.ColumnTypeText),
crdb.NewColumn(LDAPHostCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPPortCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPTlsCol, crdb.ColumnTypeBool, crdb.Nullable()),
crdb.NewColumn(LDAPBaseDNCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPUserObjectClassCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPUserUniqueAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPAdminCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPPasswordCol, crdb.ColumnTypeJSONB, crdb.Nullable()),
crdb.NewColumn(LDAPIDAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPFirstNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPLastNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPDisplayNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPNickNameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPPreferredUsernameAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPEmailAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPEmailVerifiedAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPPhoneAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPPhoneVerifiedAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPPreferredLanguageAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPAvatarURLAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(LDAPProfileAttributeCol, crdb.ColumnTypeText, crdb.Nullable()),
},
crdb.NewPrimaryKey(LDAPInstanceIDCol, LDAPIDCol),
IDPTemplateLDAPSuffix,
crdb.WithForeignKey(crdb.NewForeignKeyOfPublicKeys()),
),
)
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
}
func (p *idpTemplateProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: instance.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: instance.LDAPIDPAddedEventType,
Reduce: p.reduceLDAPIDPAdded,
},
{
Event: instance.LDAPIDPChangedEventType,
Reduce: p.reduceLDAPIDPChanged,
},
{
Event: instance.IDPRemovedEventType,
Reduce: p.reduceIDPRemoved,
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(IDPTemplateInstanceIDCol),
},
},
},
{
Aggregate: org.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: org.LDAPIDPAddedEventType,
Reduce: p.reduceLDAPIDPAdded,
},
{
Event: org.LDAPIDPChangedEventType,
Reduce: p.reduceLDAPIDPChanged,
},
{
Event: org.IDPRemovedEventType,
Reduce: p.reduceIDPRemoved,
},
{
Event: org.OrgRemovedEventType,
Reduce: p.reduceOwnerRemoved,
},
},
},
}
}
func (p *idpTemplateProjection) reduceLDAPIDPAdded(event eventstore.Event) (*handler.Statement, error) {
var idpEvent idp.LDAPIDPAddedEvent
var idpOwnerType domain.IdentityProviderType
switch e := event.(type) {
case *org.LDAPIDPAddedEvent:
idpEvent = e.LDAPIDPAddedEvent
idpOwnerType = domain.IdentityProviderTypeOrg
case *instance.LDAPIDPAddedEvent:
idpEvent = e.LDAPIDPAddedEvent
idpOwnerType = domain.IdentityProviderTypeSystem
default:
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-9s02m1", "reduce.wrong.event.type %v", []eventstore.EventType{org.LDAPIDPAddedEventType, instance.LDAPIDPAddedEventType})
}
return crdb.NewMultiStatement(
&idpEvent,
crdb.AddCreateStatement(
[]handler.Column{
handler.NewCol(IDPTemplateIDCol, idpEvent.ID),
handler.NewCol(IDPTemplateCreationDateCol, idpEvent.CreationDate()),
handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()),
handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()),
handler.NewCol(IDPTemplateResourceOwnerCol, idpEvent.Aggregate().ResourceOwner),
handler.NewCol(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive),
handler.NewCol(IDPTemplateNameCol, idpEvent.Name),
handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType),
handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeLDAP),
handler.NewCol(IDPTemplateIsCreationAllowedCol, idpEvent.IsCreationAllowed),
handler.NewCol(IDPTemplateIsLinkingAllowedCol, idpEvent.IsLinkingAllowed),
handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.IsAutoCreation),
handler.NewCol(IDPTemplateIsAutoUpdateCol, idpEvent.IsAutoUpdate),
},
),
crdb.AddCreateStatement(
[]handler.Column{
handler.NewCol(LDAPIDCol, idpEvent.ID),
handler.NewCol(LDAPInstanceIDCol, idpEvent.Aggregate().InstanceID),
handler.NewCol(LDAPHostCol, idpEvent.Host),
handler.NewCol(LDAPPortCol, idpEvent.Port),
handler.NewCol(LDAPTlsCol, idpEvent.TLS),
handler.NewCol(LDAPBaseDNCol, idpEvent.BaseDN),
handler.NewCol(LDAPUserObjectClassCol, idpEvent.UserObjectClass),
handler.NewCol(LDAPUserUniqueAttributeCol, idpEvent.UserUniqueAttribute),
handler.NewCol(LDAPAdminCol, idpEvent.Admin),
handler.NewCol(LDAPPasswordCol, idpEvent.Password),
handler.NewCol(LDAPIDAttributeCol, idpEvent.IDAttribute),
handler.NewCol(LDAPFirstNameAttributeCol, idpEvent.FirstNameAttribute),
handler.NewCol(LDAPLastNameAttributeCol, idpEvent.LastNameAttribute),
handler.NewCol(LDAPDisplayNameAttributeCol, idpEvent.DisplayNameAttribute),
handler.NewCol(LDAPNickNameAttributeCol, idpEvent.NickNameAttribute),
handler.NewCol(LDAPPreferredUsernameAttributeCol, idpEvent.PreferredUsernameAttribute),
handler.NewCol(LDAPEmailAttributeCol, idpEvent.EmailAttribute),
handler.NewCol(LDAPEmailVerifiedAttributeCol, idpEvent.EmailVerifiedAttribute),
handler.NewCol(LDAPPhoneAttributeCol, idpEvent.PhoneAttribute),
handler.NewCol(LDAPPhoneVerifiedAttributeCol, idpEvent.PhoneVerifiedAttribute),
handler.NewCol(LDAPPreferredLanguageAttributeCol, idpEvent.PreferredLanguageAttribute),
handler.NewCol(LDAPAvatarURLAttributeCol, idpEvent.AvatarURLAttribute),
handler.NewCol(LDAPProfileAttributeCol, idpEvent.ProfileAttribute),
},
crdb.WithTableSuffix(IDPTemplateLDAPSuffix),
),
), nil
}
func (p *idpTemplateProjection) reduceLDAPIDPChanged(event eventstore.Event) (*handler.Statement, error) {
var idpEvent idp.LDAPIDPChangedEvent
switch e := event.(type) {
case *org.LDAPIDPChangedEvent:
idpEvent = e.LDAPIDPChangedEvent
case *instance.LDAPIDPChangedEvent:
idpEvent = e.LDAPIDPChangedEvent
default:
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-p1582ks", "reduce.wrong.event.type %v", []eventstore.EventType{org.LDAPIDPChangedEventType, instance.LDAPIDPChangedEventType})
}
cols := reduceLDAPIDPChangedTemplateColumns(idpEvent)
ldapCols := reduceLDAPIDPChangedLDAPColumns(idpEvent)
ops := make([]func(eventstore.Event) crdb.Exec, 0, 2)
ops = append(ops,
crdb.AddUpdateStatement(
cols,
[]handler.Condition{
handler.NewCond(IDPTemplateIDCol, idpEvent.ID),
handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
},
),
)
if len(ldapCols) > 0 {
ops = append(ops,
crdb.AddUpdateStatement(
ldapCols,
[]handler.Condition{
handler.NewCond(LDAPIDCol, idpEvent.ID),
handler.NewCond(LDAPInstanceIDCol, idpEvent.Aggregate().InstanceID),
},
crdb.WithTableSuffix(IDPTemplateLDAPSuffix),
),
)
}
return crdb.NewMultiStatement(
&idpEvent,
ops...,
), nil
}
func (p *idpTemplateProjection) reduceIDPRemoved(event eventstore.Event) (*handler.Statement, error) {
var idpEvent idp.RemovedEvent
switch e := event.(type) {
case *org.IDPRemovedEvent:
idpEvent = e.RemovedEvent
case *instance.IDPRemovedEvent:
idpEvent = e.RemovedEvent
default:
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-xbcvwin2", "reduce.wrong.event.type %v", []eventstore.EventType{org.IDPRemovedEventType, instance.IDPRemovedEventType})
}
return crdb.NewDeleteStatement(
&idpEvent,
[]handler.Condition{
handler.NewCond(IDPTemplateIDCol, idpEvent.ID),
handler.NewCond(IDPTemplateInstanceIDCol, idpEvent.Aggregate().InstanceID),
},
), nil
}
func (p *idpTemplateProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.OrgRemovedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-Jp0D2K", "reduce.wrong.event.type %s", org.OrgRemovedEventType)
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(IDPTemplateChangeDateCol, e.CreationDate()),
handler.NewCol(IDPTemplateSequenceCol, e.Sequence()),
handler.NewCol(IDPTemplateOwnerRemovedCol, true),
},
[]handler.Condition{
handler.NewCond(IDPTemplateInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(IDPTemplateResourceOwnerCol, e.Aggregate().ID),
},
), nil
}
func reduceLDAPIDPChangedTemplateColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column {
cols := make([]handler.Column, 0, 7)
if idpEvent.Name != nil {
cols = append(cols, handler.NewCol(IDPTemplateNameCol, *idpEvent.Name))
}
if idpEvent.IsCreationAllowed != nil {
cols = append(cols, handler.NewCol(IDPTemplateIsCreationAllowedCol, *idpEvent.IsCreationAllowed))
}
if idpEvent.IsLinkingAllowed != nil {
cols = append(cols, handler.NewCol(IDPTemplateIsLinkingAllowedCol, *idpEvent.IsLinkingAllowed))
}
if idpEvent.IsAutoCreation != nil {
cols = append(cols, handler.NewCol(IDPTemplateIsAutoCreationCol, *idpEvent.IsAutoCreation))
}
if idpEvent.IsAutoUpdate != nil {
cols = append(cols, handler.NewCol(IDPTemplateIsAutoUpdateCol, *idpEvent.IsAutoUpdate))
}
return append(cols,
handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()),
handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()),
)
}
func reduceLDAPIDPChangedLDAPColumns(idpEvent idp.LDAPIDPChangedEvent) []handler.Column {
ldapCols := make([]handler.Column, 0, 4)
if idpEvent.Host != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPHostCol, *idpEvent.Host))
}
if idpEvent.Port != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPPortCol, *idpEvent.Port))
}
if idpEvent.TLS != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPTlsCol, *idpEvent.TLS))
}
if idpEvent.BaseDN != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPBaseDNCol, *idpEvent.BaseDN))
}
if idpEvent.UserObjectClass != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPUserObjectClassCol, *idpEvent.UserObjectClass))
}
if idpEvent.UserUniqueAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPUserUniqueAttributeCol, *idpEvent.UserUniqueAttribute))
}
if idpEvent.Admin != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPAdminCol, *idpEvent.Admin))
}
if idpEvent.Password != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPPasswordCol, *idpEvent.Password))
}
if idpEvent.IDAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPIDAttributeCol, *idpEvent.IDAttribute))
}
if idpEvent.FirstNameAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPFirstNameAttributeCol, *idpEvent.FirstNameAttribute))
}
if idpEvent.LastNameAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPLastNameAttributeCol, *idpEvent.LastNameAttribute))
}
if idpEvent.DisplayNameAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPDisplayNameAttributeCol, *idpEvent.DisplayNameAttribute))
}
if idpEvent.NickNameAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPNickNameAttributeCol, *idpEvent.NickNameAttribute))
}
if idpEvent.PreferredUsernameAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPPreferredUsernameAttributeCol, *idpEvent.PreferredUsernameAttribute))
}
if idpEvent.EmailAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPEmailAttributeCol, *idpEvent.EmailAttribute))
}
if idpEvent.EmailVerifiedAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPEmailVerifiedAttributeCol, *idpEvent.EmailVerifiedAttribute))
}
if idpEvent.PhoneAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPPhoneAttributeCol, *idpEvent.PhoneAttribute))
}
if idpEvent.PhoneVerifiedAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPPhoneVerifiedAttributeCol, *idpEvent.PhoneVerifiedAttribute))
}
if idpEvent.PreferredLanguageAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPPreferredLanguageAttributeCol, *idpEvent.PreferredLanguageAttribute))
}
if idpEvent.AvatarURLAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPAvatarURLAttributeCol, *idpEvent.AvatarURLAttribute))
}
if idpEvent.ProfileAttribute != nil {
ldapCols = append(ldapCols, handler.NewCol(LDAPProfileAttributeCol, *idpEvent.ProfileAttribute))
}
return ldapCols
}

View File

@@ -0,0 +1,515 @@
package projection
import (
"testing"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
)
func TestIDPTemplateProjection_reducesRemove(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.Event
}
tests := []struct {
name string
args args
reduce func(event eventstore.Event) (*handler.Statement, error)
want wantReduce
}{
{
name: "instance reduceInstanceRemoved",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.InstanceRemovedEventType),
instance.AggregateType,
nil,
), instance.InstanceRemovedEventMapper),
},
reduce: reduceInstanceRemovedHelper(IDPInstanceIDCol),
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.idp_templates WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
},
},
},
},
},
{
name: "org reduceOwnerRemoved",
reduce: (&idpTemplateProjection{}).reduceOwnerRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgRemovedEventType),
org.AggregateType,
nil,
), org.OrgRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.idp_templates SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"instance-id",
"agg-id",
},
},
},
},
},
},
{
name: "org reduceIDPRemoved",
reduce: (&idpTemplateProjection{}).reduceIDPRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.IDPRemovedEventType),
org.AggregateType,
[]byte(`{
"id": "idp-id"
}`),
), org.IDPRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.idp_templates WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if !errors.IsErrorInvalidArgument(err) {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, IDPTemplateTable, tt.want)
})
}
}
func TestIDPTemplateProjection_reducesLDAP(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.Event
}
tests := []struct {
name string
args args
reduce func(event eventstore.Event) (*handler.Statement, error)
want wantReduce
}{
{
name: "instance reduceLDAPIDPAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.LDAPIDPAddedEventType),
instance.AggregateType,
[]byte(`{
"id": "idp-id",
"name": "custom-zitadel-instance",
"host": "host",
"port": "port",
"tls": true,
"baseDN": "base",
"userObjectClass": "user",
"userUniqueAttribute": "uid",
"admin": "admin",
"password": {
"cryptoType": 0,
"algorithm": "RSA-265",
"keyId": "key-id"
},
"idAttribute": "id",
"firstNameAttribute": "first",
"lastNameAttribute": "last",
"displayNameAttribute": "display",
"nickNameAttribute": "nickname",
"preferredUsernameAttribute": "username",
"emailAttribute": "email",
"emailVerifiedAttribute": "email_verified",
"phoneAttribute": "phone",
"phoneVerifiedAttribute": "phone_verified",
"preferredLanguageAttribute": "lang",
"avatarURLAttribute": "avatar",
"profileAttribute": "profile",
"isCreationAllowed": true,
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true
}`),
), instance.LDAPIDPAddedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceLDAPIDPAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.idp_templates (id, creation_date, change_date, sequence, resource_owner, instance_id, state, name, owner_type, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
expectedArgs: []interface{}{
"idp-id",
anyArg{},
anyArg{},
uint64(15),
"ro-id",
"instance-id",
domain.IDPStateActive,
"custom-zitadel-instance",
domain.IdentityProviderTypeSystem,
domain.IDPTypeLDAP,
true,
true,
true,
true,
},
},
{
expectedStmt: "INSERT INTO projections.idp_templates_ldap (idp_id, instance_id, host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
"host",
"port",
true,
"base",
"user",
"uid",
"admin",
anyArg{},
"id",
"first",
"last",
"display",
"nickname",
"username",
"email",
"email_verified",
"phone",
"phone_verified",
"lang",
"avatar",
"profile",
},
},
},
},
},
},
{
name: "org reduceLDAPIDPAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(org.LDAPIDPAddedEventType),
org.AggregateType,
[]byte(`{
"id": "idp-id",
"name": "custom-zitadel-instance",
"host": "host",
"port": "port",
"tls": true,
"baseDN": "base",
"userObjectClass": "user",
"userUniqueAttribute": "uid",
"admin": "admin",
"password": {
"cryptoType": 0,
"algorithm": "RSA-265",
"keyId": "key-id"
},
"idAttribute": "id",
"firstNameAttribute": "first",
"lastNameAttribute": "last",
"displayNameAttribute": "display",
"nickNameAttribute": "nickname",
"preferredUsernameAttribute": "username",
"emailAttribute": "email",
"emailVerifiedAttribute": "email_verified",
"phoneAttribute": "phone",
"phoneVerifiedAttribute": "phone_verified",
"preferredLanguageAttribute": "lang",
"avatarURLAttribute": "avatar",
"profileAttribute": "profile",
"isCreationAllowed": true,
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true
}`),
), org.LDAPIDPAddedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceLDAPIDPAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.idp_templates (id, creation_date, change_date, sequence, resource_owner, instance_id, state, name, owner_type, type, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)",
expectedArgs: []interface{}{
"idp-id",
anyArg{},
anyArg{},
uint64(15),
"ro-id",
"instance-id",
domain.IDPStateActive,
"custom-zitadel-instance",
domain.IdentityProviderTypeOrg,
domain.IDPTypeLDAP,
true,
true,
true,
true,
},
},
{
expectedStmt: "INSERT INTO projections.idp_templates_ldap (idp_id, instance_id, host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)",
expectedArgs: []interface{}{
"idp-id",
"instance-id",
"host",
"port",
true,
"base",
"user",
"uid",
"admin",
anyArg{},
"id",
"first",
"last",
"display",
"nickname",
"username",
"email",
"email_verified",
"phone",
"phone_verified",
"lang",
"avatar",
"profile",
},
},
},
},
},
},
{
name: "instance reduceLDAPIDPChanged minimal",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.LDAPIDPChangedEventType),
instance.AggregateType,
[]byte(`{
"id": "idp-id",
"name": "custom-zitadel-instance",
"host": "host"
}`),
), instance.LDAPIDPChangedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceLDAPIDPChanged,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.idp_templates SET (name, change_date, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
"custom-zitadel-instance",
anyArg{},
uint64(15),
"idp-id",
"instance-id",
},
},
{
expectedStmt: "UPDATE projections.idp_templates_ldap SET host = $1 WHERE (idp_id = $2) AND (instance_id = $3)",
expectedArgs: []interface{}{
"host",
"idp-id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceLDAPIDPChanged",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.LDAPIDPChangedEventType),
instance.AggregateType,
[]byte(`{
"id": "idp-id",
"name": "custom-zitadel-instance",
"host": "host",
"port": "port",
"tls": true,
"baseDN": "base",
"userObjectClass": "user",
"userUniqueAttribute": "uid",
"admin": "admin",
"password": {
"cryptoType": 0,
"algorithm": "RSA-265",
"keyId": "key-id"
},
"idAttribute": "id",
"firstNameAttribute": "first",
"lastNameAttribute": "last",
"displayNameAttribute": "display",
"nickNameAttribute": "nickname",
"preferredUsernameAttribute": "username",
"emailAttribute": "email",
"emailVerifiedAttribute": "email_verified",
"phoneAttribute": "phone",
"phoneVerifiedAttribute": "phone_verified",
"preferredLanguageAttribute": "lang",
"avatarURLAttribute": "avatar",
"profileAttribute": "profile",
"isCreationAllowed": true,
"isLinkingAllowed": true,
"isAutoCreation": true,
"isAutoUpdate": true
}`),
), instance.LDAPIDPChangedEventMapper),
},
reduce: (&idpTemplateProjection{}).reduceLDAPIDPChanged,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.idp_templates SET (name, is_creation_allowed, is_linking_allowed, is_auto_creation, is_auto_update, change_date, sequence) = ($1, $2, $3, $4, $5, $6, $7) WHERE (id = $8) AND (instance_id = $9)",
expectedArgs: []interface{}{
"custom-zitadel-instance",
true,
true,
true,
true,
anyArg{},
uint64(15),
"idp-id",
"instance-id",
},
},
{
expectedStmt: "UPDATE projections.idp_templates_ldap SET (host, port, tls, base_dn, user_object_class, user_unique_attribute, admin, password, id_attribute, first_name_attribute, last_name_attribute, display_name_attribute, nick_name_attribute, preferred_username_attribute, email_attribute, email_verified, phone_attribute, phone_verified_attribute, preferred_language_attribute, avatar_url_attribute, profile_attribute) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) WHERE (idp_id = $22) AND (instance_id = $23)",
expectedArgs: []interface{}{
"host",
"port",
true,
"base",
"user",
"uid",
"admin",
anyArg{},
"id",
"first",
"last",
"display",
"nickname",
"username",
"email",
"email_verified",
"phone",
"phone_verified",
"lang",
"avatar",
"profile",
"idp-id",
"instance-id",
},
},
},
},
},
},
{
name: "org.reduceOwnerRemoved",
reduce: (&idpProjection{}).reduceOwnerRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgRemovedEventType),
org.AggregateType,
nil,
), org.OrgRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.idp_templates SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"instance-id",
"agg-id",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if !errors.IsErrorInvalidArgument(err) {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, IDPTemplateTable, tt.want)
})
}
}

View File

@@ -37,6 +37,7 @@ var (
AppProjection *appProjection
IDPUserLinkProjection *idpUserLinkProjection
IDPLoginPolicyLinkProjection *idpLoginPolicyLinkProjection
IDPTemplateProjection *idpTemplateProjection
MailTemplateProjection *mailTemplateProjection
MessageTextProjection *messageTextProjection
CustomTextProjection *customTextProjection
@@ -111,6 +112,7 @@ func Create(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, c
AppProjection = newAppProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["apps"]))
IDPUserLinkProjection = newIDPUserLinkProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_user_links"]))
IDPLoginPolicyLinkProjection = newIDPLoginPolicyLinkProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_login_policy_links"]))
IDPTemplateProjection = newIDPTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_templates"]))
MailTemplateProjection = newMailTemplateProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["mail_templates"]))
MessageTextProjection = newMessageTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["message_texts"]))
CustomTextProjection = newCustomTextProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["custom_texts"]))
@@ -199,6 +201,7 @@ func newProjectionsList() {
OrgDomainProjection,
LoginPolicyProjection,
IDPProjection,
IDPTemplateProjection,
AppProjection,
IDPUserLinkProjection,
IDPLoginPolicyLinkProjection,

View File

@@ -0,0 +1,104 @@
package idp
import (
"encoding/json"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
)
type Options struct {
IsCreationAllowed bool `json:"isCreationAllowed,omitempty"`
IsLinkingAllowed bool `json:"isLinkingAllowed,omitempty"`
IsAutoCreation bool `json:"isAutoCreation,omitempty"`
IsAutoUpdate bool `json:"isAutoUpdate,omitempty"`
}
type OptionChanges struct {
IsCreationAllowed *bool `json:"isCreationAllowed,omitempty"`
IsLinkingAllowed *bool `json:"isLinkingAllowed,omitempty"`
IsAutoCreation *bool `json:"isAutoCreation,omitempty"`
IsAutoUpdate *bool `json:"isAutoUpdate,omitempty"`
}
func (o *Options) Changes(options Options) OptionChanges {
opts := OptionChanges{}
if o.IsCreationAllowed != options.IsCreationAllowed {
opts.IsCreationAllowed = &options.IsCreationAllowed
}
if o.IsLinkingAllowed != options.IsLinkingAllowed {
opts.IsLinkingAllowed = &options.IsLinkingAllowed
}
if o.IsAutoCreation != options.IsAutoCreation {
opts.IsAutoCreation = &options.IsAutoCreation
}
if o.IsAutoUpdate != options.IsAutoUpdate {
opts.IsAutoUpdate = &options.IsAutoUpdate
}
return opts
}
func (o *Options) ReduceChanges(changes OptionChanges) {
if changes.IsCreationAllowed != nil {
o.IsCreationAllowed = *changes.IsCreationAllowed
}
if changes.IsLinkingAllowed != nil {
o.IsLinkingAllowed = *changes.IsLinkingAllowed
}
if changes.IsAutoUpdate != nil {
o.IsAutoUpdate = *changes.IsAutoUpdate
}
if changes.IsAutoUpdate != nil {
o.IsAutoUpdate = *changes.IsAutoUpdate
}
}
func (o *OptionChanges) IsZero() bool {
return o.IsCreationAllowed == nil && o.IsLinkingAllowed == nil && o.IsAutoCreation == nil && o.IsAutoUpdate == nil
}
type RemovedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
name string
}
func NewRemovedEvent(
base *eventstore.BaseEvent,
id string,
name string,
) *RemovedEvent {
return &RemovedEvent{
BaseEvent: *base,
ID: id,
name: name,
}
}
func (e *RemovedEvent) Data() interface{} {
return e
}
func (e *RemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
if e.name == "" {
return nil
}
return []*eventstore.EventUniqueConstraint{idpconfig.NewRemoveIDPConfigNameUniqueConstraint(e.name, e.Aggregate().ResourceOwner)}
}
func RemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &RemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "IDP-plSD2", "unable to unmarshal event")
}
return e, nil
}

View File

@@ -0,0 +1,351 @@
package idp
import (
"encoding/json"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
)
type LDAPIDPAddedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Port string `json:"port,omitempty"`
TLS bool `json:"tls"`
BaseDN string `json:"baseDN"`
UserObjectClass string `json:"userObjectClass"`
UserUniqueAttribute string `json:"userUniqueAttribute"`
Admin string `json:"admin"`
Password *crypto.CryptoValue `json:"password"`
LDAPAttributes
Options
}
type LDAPAttributes struct {
IDAttribute string `json:"idAttribute,omitempty"`
FirstNameAttribute string `json:"firstNameAttribute,omitempty"`
LastNameAttribute string `json:"lastNameAttribute,omitempty"`
DisplayNameAttribute string `json:"displayNameAttribute,omitempty"`
NickNameAttribute string `json:"nickNameAttribute,omitempty"`
PreferredUsernameAttribute string `json:"preferredUsernameAttribute,omitempty"`
EmailAttribute string `json:"emailAttribute,omitempty"`
EmailVerifiedAttribute string `json:"emailVerifiedAttribute,omitempty"`
PhoneAttribute string `json:"phoneAttribute,omitempty"`
PhoneVerifiedAttribute string `json:"phoneVerifiedAttribute,omitempty"`
PreferredLanguageAttribute string `json:"preferredLanguageAttribute,omitempty"`
AvatarURLAttribute string `json:"avatarURLAttribute,omitempty"`
ProfileAttribute string `json:"profileAttribute,omitempty"`
}
func (o *LDAPAttributes) Changes(attributes LDAPAttributes) LDAPAttributeChanges {
attrs := LDAPAttributeChanges{}
if o.IDAttribute != attributes.IDAttribute {
attrs.IDAttribute = &attributes.IDAttribute
}
if o.FirstNameAttribute != attributes.FirstNameAttribute {
attrs.FirstNameAttribute = &attributes.FirstNameAttribute
}
if o.LastNameAttribute != attributes.LastNameAttribute {
attrs.LastNameAttribute = &attributes.LastNameAttribute
}
if o.DisplayNameAttribute != attributes.DisplayNameAttribute {
attrs.DisplayNameAttribute = &attributes.DisplayNameAttribute
}
if o.NickNameAttribute != attributes.NickNameAttribute {
attrs.NickNameAttribute = &attributes.NickNameAttribute
}
if o.PreferredUsernameAttribute != attributes.PreferredUsernameAttribute {
attrs.PreferredUsernameAttribute = &attributes.PreferredUsernameAttribute
}
if o.EmailAttribute != attributes.EmailAttribute {
attrs.EmailAttribute = &attributes.EmailAttribute
}
if o.EmailVerifiedAttribute != attributes.EmailVerifiedAttribute {
attrs.EmailVerifiedAttribute = &attributes.EmailVerifiedAttribute
}
if o.PhoneAttribute != attributes.PhoneAttribute {
attrs.PhoneAttribute = &attributes.PhoneAttribute
}
if o.PhoneVerifiedAttribute != attributes.PhoneVerifiedAttribute {
attrs.PhoneVerifiedAttribute = &attributes.PhoneVerifiedAttribute
}
if o.PreferredLanguageAttribute != attributes.PreferredLanguageAttribute {
attrs.PreferredLanguageAttribute = &attributes.PreferredLanguageAttribute
}
if o.AvatarURLAttribute != attributes.AvatarURLAttribute {
attrs.AvatarURLAttribute = &attributes.AvatarURLAttribute
}
if o.ProfileAttribute != attributes.ProfileAttribute {
attrs.ProfileAttribute = &attributes.ProfileAttribute
}
return attrs
}
func (o *LDAPAttributes) ReduceChanges(changes LDAPAttributeChanges) {
if changes.IDAttribute != nil {
o.IDAttribute = *changes.IDAttribute
}
if changes.FirstNameAttribute != nil {
o.FirstNameAttribute = *changes.FirstNameAttribute
}
if changes.LastNameAttribute != nil {
o.LastNameAttribute = *changes.LastNameAttribute
}
if changes.DisplayNameAttribute != nil {
o.DisplayNameAttribute = *changes.DisplayNameAttribute
}
if changes.NickNameAttribute != nil {
o.NickNameAttribute = *changes.NickNameAttribute
}
if changes.PreferredUsernameAttribute != nil {
o.PreferredUsernameAttribute = *changes.PreferredUsernameAttribute
}
if changes.EmailAttribute != nil {
o.EmailAttribute = *changes.EmailAttribute
}
if changes.EmailVerifiedAttribute != nil {
o.EmailVerifiedAttribute = *changes.EmailVerifiedAttribute
}
if changes.PhoneAttribute != nil {
o.PhoneAttribute = *changes.PhoneAttribute
}
if changes.PhoneVerifiedAttribute != nil {
o.PhoneVerifiedAttribute = *changes.PhoneVerifiedAttribute
}
if changes.PreferredLanguageAttribute != nil {
o.PreferredLanguageAttribute = *changes.PreferredLanguageAttribute
}
if changes.AvatarURLAttribute != nil {
o.AvatarURLAttribute = *changes.AvatarURLAttribute
}
if changes.ProfileAttribute != nil {
o.ProfileAttribute = *changes.ProfileAttribute
}
}
func NewLDAPIDPAddedEvent(
base *eventstore.BaseEvent,
id,
name,
host,
port string,
tls bool,
baseDN,
userObjectClass,
userUniqueAttribute,
admin string,
password *crypto.CryptoValue,
attributes LDAPAttributes,
options Options,
) *LDAPIDPAddedEvent {
return &LDAPIDPAddedEvent{
BaseEvent: *base,
ID: id,
Name: name,
Host: host,
Port: port,
TLS: tls,
BaseDN: baseDN,
UserObjectClass: userObjectClass,
UserUniqueAttribute: userUniqueAttribute,
Admin: admin,
Password: password,
LDAPAttributes: attributes,
Options: options,
}
}
func (e *LDAPIDPAddedEvent) Data() interface{} {
return e
}
func (e *LDAPIDPAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return []*eventstore.EventUniqueConstraint{idpconfig.NewAddIDPConfigNameUniqueConstraint(e.Name, e.Aggregate().ResourceOwner)}
}
func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &LDAPIDPAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "IDP-Dgh42", "unable to unmarshal event")
}
return e, nil
}
type LDAPIDPChangedEvent struct {
eventstore.BaseEvent `json:"-"`
oldName string
ID string `json:"id"`
Name *string `json:"name,omitempty"`
Host *string `json:"host,omitempty"`
Port *string `json:"port,omitempty"`
TLS *bool `json:"tls,omitempty"`
BaseDN *string `json:"baseDN,omitempty"`
UserObjectClass *string `json:"userObjectClass,omitempty"`
UserUniqueAttribute *string `json:"userUniqueAttribute,omitempty"`
Admin *string `json:"admin,omitempty"`
Password *crypto.CryptoValue `json:"password,omitempty"`
LDAPAttributeChanges
OptionChanges
}
type LDAPAttributeChanges struct {
IDAttribute *string `json:"idAttribute,omitempty"`
FirstNameAttribute *string `json:"firstNameAttribute,omitempty"`
LastNameAttribute *string `json:"lastNameAttribute,omitempty"`
DisplayNameAttribute *string `json:"displayNameAttribute,omitempty"`
NickNameAttribute *string `json:"nickNameAttribute,omitempty"`
PreferredUsernameAttribute *string `json:"preferredUsernameAttribute,omitempty"`
EmailAttribute *string `json:"emailAttribute,omitempty"`
EmailVerifiedAttribute *string `json:"emailVerifiedAttribute,omitempty"`
PhoneAttribute *string `json:"phoneAttribute,omitempty"`
PhoneVerifiedAttribute *string `json:"phoneVerifiedAttribute,omitempty"`
PreferredLanguageAttribute *string `json:"preferredLanguageAttribute,omitempty"`
AvatarURLAttribute *string `json:"avatarURLAttribute,omitempty"`
ProfileAttribute *string `json:"profileAttribute,omitempty"`
}
func (o LDAPAttributeChanges) IsZero() bool {
return o.IDAttribute == nil &&
o.FirstNameAttribute == nil &&
o.LastNameAttribute == nil &&
o.DisplayNameAttribute == nil &&
o.NickNameAttribute == nil &&
o.PreferredUsernameAttribute == nil &&
o.EmailAttribute == nil &&
o.EmailVerifiedAttribute == nil &&
o.PhoneAttribute == nil &&
o.PhoneVerifiedAttribute == nil &&
o.PreferredLanguageAttribute == nil &&
o.AvatarURLAttribute == nil &&
o.ProfileAttribute == nil
}
func NewLDAPIDPChangedEvent(
base *eventstore.BaseEvent,
id string,
oldName string,
changes []LDAPIDPChanges,
) (*LDAPIDPChangedEvent, error) {
if len(changes) == 0 {
return nil, errors.ThrowPreconditionFailed(nil, "IDP-SDf3f", "Errors.NoChangesFound")
}
changedEvent := &LDAPIDPChangedEvent{
BaseEvent: *base,
ID: id,
oldName: oldName,
}
for _, change := range changes {
change(changedEvent)
}
return changedEvent, nil
}
type LDAPIDPChanges func(*LDAPIDPChangedEvent)
func ChangeLDAPName(name string) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.Name = &name
}
}
func ChangeLDAPHost(host string) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.Host = &host
}
}
func ChangeLDAPPort(port string) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.Port = &port
}
}
func ChangeLDAPTLS(tls bool) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.TLS = &tls
}
}
func ChangeLDAPBaseDN(basDN string) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.BaseDN = &basDN
}
}
func ChangeLDAPUserObjectClass(userObjectClass string) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.UserObjectClass = &userObjectClass
}
}
func ChangeLDAPUserUniqueAttribute(userUniqueAttribute string) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.UserUniqueAttribute = &userUniqueAttribute
}
}
func ChangeLDAPAdmin(admin string) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.Admin = &admin
}
}
func ChangeLDAPPassword(password *crypto.CryptoValue) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.Password = password
}
}
func ChangeLDAPAttributes(attributes LDAPAttributeChanges) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.LDAPAttributeChanges = attributes
}
}
func ChangeLDAPOptions(options OptionChanges) func(*LDAPIDPChangedEvent) {
return func(e *LDAPIDPChangedEvent) {
e.OptionChanges = options
}
}
func (e *LDAPIDPChangedEvent) Data() interface{} {
return e
}
func (e *LDAPIDPChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
if e.Name == nil || e.oldName == *e.Name { // TODO: nil check should be enough?
return nil
}
return []*eventstore.EventUniqueConstraint{
idpconfig.NewRemoveIDPConfigNameUniqueConstraint(e.oldName, e.Aggregate().ResourceOwner),
idpconfig.NewAddIDPConfigNameUniqueConstraint(*e.Name, e.Aggregate().ResourceOwner),
}
}
func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &LDAPIDPChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "IDP-Sfth3", "unable to unmarshal event")
}
return e, nil
}

View File

@@ -70,6 +70,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderAddedEventType, IdentityProviderAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderRemovedEventType, IdentityProviderRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, LoginPolicyIDPProviderCascadeRemovedEventType, IdentityProviderCascadeRemovedEventMapper).

View File

@@ -0,0 +1,142 @@
package instance
import (
"context"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/idp"
)
const (
LDAPIDPAddedEventType eventstore.EventType = "instance.idp.ldap.added"
LDAPIDPChangedEventType eventstore.EventType = "instance.idp.ldap.changed"
IDPRemovedEventType eventstore.EventType = "instance.idp.removed"
)
type LDAPIDPAddedEvent struct {
idp.LDAPIDPAddedEvent
}
func NewLDAPIDPAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
host,
port string,
tls bool,
baseDN,
userObjectClass,
userUniqueAttribute,
admin string,
password *crypto.CryptoValue,
attributes idp.LDAPAttributes,
options idp.Options,
) *LDAPIDPAddedEvent {
return &LDAPIDPAddedEvent{
LDAPIDPAddedEvent: *idp.NewLDAPIDPAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
LDAPIDPAddedEventType,
),
id,
name,
host,
port,
tls,
baseDN,
userObjectClass,
userUniqueAttribute,
admin,
password,
attributes,
options,
),
}
}
func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.LDAPIDPAddedEventMapper(event)
if err != nil {
return nil, err
}
return &LDAPIDPAddedEvent{LDAPIDPAddedEvent: *e.(*idp.LDAPIDPAddedEvent)}, nil
}
type LDAPIDPChangedEvent struct {
idp.LDAPIDPChangedEvent
}
func NewLDAPIDPChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
oldName string,
changes []idp.LDAPIDPChanges,
) (*LDAPIDPChangedEvent, error) {
changedEvent, err := idp.NewLDAPIDPChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
LDAPIDPChangedEventType,
),
id,
oldName,
changes,
)
if err != nil {
return nil, err
}
return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *changedEvent}, nil
}
func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.LDAPIDPChangedEventMapper(event)
if err != nil {
return nil, err
}
return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil
}
type IDPRemovedEvent struct {
idp.RemovedEvent
}
func NewIDPRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
name string,
) *IDPRemovedEvent {
return &IDPRemovedEvent{
RemovedEvent: *idp.NewRemovedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
IDPRemovedEventType,
),
id,
name,
),
}
}
func (e *IDPRemovedEvent) Data() interface{} {
return e
}
func IDPRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.RemovedEventMapper(event)
if err != nil {
return nil, err
}
return &IDPRemovedEvent{RemovedEvent: *e.(*idp.RemovedEvent)}, nil
}

View File

@@ -78,6 +78,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, IDPOIDCConfigChangedEventType, IDPOIDCConfigChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigAddedEventType, IDPJWTConfigAddedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPAddedEventType, LDAPIDPAddedEventMapper).
RegisterFilterEventMapper(AggregateType, LDAPIDPChangedEventType, LDAPIDPChangedEventMapper).
RegisterFilterEventMapper(AggregateType, IDPRemovedEventType, IDPRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, TriggerActionsSetEventType, TriggerActionsSetEventMapper).
RegisterFilterEventMapper(AggregateType, TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, FlowClearedEventType, FlowClearedEventMapper).

View File

@@ -0,0 +1,142 @@
package org
import (
"context"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/idp"
)
const (
LDAPIDPAddedEventType eventstore.EventType = "org.idp.ldap.added"
LDAPIDPChangedEventType eventstore.EventType = "org.idp.ldap.changed"
IDPRemovedEventType eventstore.EventType = "org.idp.removed"
)
type LDAPIDPAddedEvent struct {
idp.LDAPIDPAddedEvent
}
func NewLDAPIDPAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
name,
host,
port string,
tls bool,
baseDN,
userObjectClass,
userUniqueAttribute,
admin string,
password *crypto.CryptoValue,
attributes idp.LDAPAttributes,
options idp.Options,
) *LDAPIDPAddedEvent {
return &LDAPIDPAddedEvent{
LDAPIDPAddedEvent: *idp.NewLDAPIDPAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
LDAPIDPAddedEventType,
),
id,
name,
host,
port,
tls,
baseDN,
userObjectClass,
userUniqueAttribute,
admin,
password,
attributes,
options,
),
}
}
func LDAPIDPAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.LDAPIDPAddedEventMapper(event)
if err != nil {
return nil, err
}
return &LDAPIDPAddedEvent{LDAPIDPAddedEvent: *e.(*idp.LDAPIDPAddedEvent)}, nil
}
type LDAPIDPChangedEvent struct {
idp.LDAPIDPChangedEvent
}
func NewLDAPIDPChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id,
oldName string,
changes []idp.LDAPIDPChanges,
) (*LDAPIDPChangedEvent, error) {
changedEvent, err := idp.NewLDAPIDPChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
LDAPIDPChangedEventType,
),
id,
oldName,
changes,
)
if err != nil {
return nil, err
}
return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *changedEvent}, nil
}
func LDAPIDPChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.LDAPIDPChangedEventMapper(event)
if err != nil {
return nil, err
}
return &LDAPIDPChangedEvent{LDAPIDPChangedEvent: *e.(*idp.LDAPIDPChangedEvent)}, nil
}
type IDPRemovedEvent struct {
idp.RemovedEvent
}
func NewIDPRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
name string,
) *IDPRemovedEvent {
return &IDPRemovedEvent{
RemovedEvent: *idp.NewRemovedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
IDPRemovedEventType,
),
id,
name,
),
}
}
func (e *IDPRemovedEvent) Data() interface{} {
return e
}
func IDPRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := idp.RemovedEventMapper(event)
if err != nil {
return nil, err
}
return &IDPRemovedEvent{RemovedEvent: *e.(*idp.RemovedEvent)}, nil
}