mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 00:57:33 +00:00
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:
@@ -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
|
||||
}
|
||||
|
@@ -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),
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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),
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user