github.com/greenpau/go-authcrunch@v1.0.50/pkg/authn/portal.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package authn
    16  
    17  import (
    18  	"context"
    19  	"os"
    20  	"sort"
    21  
    22  	"github.com/greenpau/go-authcrunch/pkg/acl"
    23  	"github.com/greenpau/go-authcrunch/pkg/authn/cache"
    24  	"github.com/greenpau/go-authcrunch/pkg/authn/cookie"
    25  	"github.com/greenpau/go-authcrunch/pkg/authn/icons"
    26  	"github.com/greenpau/go-authcrunch/pkg/authn/transformer"
    27  	"github.com/greenpau/go-authcrunch/pkg/authn/ui"
    28  	"github.com/greenpau/go-authcrunch/pkg/authz/options"
    29  	"github.com/greenpau/go-authcrunch/pkg/authz/validator"
    30  	"github.com/greenpau/go-authcrunch/pkg/errors"
    31  	"github.com/greenpau/go-authcrunch/pkg/idp"
    32  	"github.com/greenpau/go-authcrunch/pkg/ids"
    33  	"github.com/greenpau/go-authcrunch/pkg/kms"
    34  	"github.com/greenpau/go-authcrunch/pkg/registry"
    35  	"github.com/greenpau/go-authcrunch/pkg/sso"
    36  	cfgutil "github.com/greenpau/go-authcrunch/pkg/util/cfg"
    37  
    38  	"fmt"
    39  	"path"
    40  	"strings"
    41  	"time"
    42  
    43  	"github.com/google/uuid"
    44  	"go.uber.org/zap"
    45  )
    46  
    47  const (
    48  	defaultPortalACLCondition = "match roles authp/admin authp/user authp/guest superuser superadmin"
    49  	defaultPortalACLAction    = "allow stop"
    50  )
    51  
    52  // Portal is an authentication portal.
    53  type Portal struct {
    54  	id                string
    55  	config            *PortalConfig
    56  	userRegistry      registry.UserRegistry
    57  	validator         *validator.TokenValidator
    58  	keystore          *kms.CryptoKeyStore
    59  	identityStores    []ids.IdentityStore
    60  	identityProviders []idp.IdentityProvider
    61  	ssoProviders      []sso.SingleSignOnProvider
    62  	cookie            *cookie.Factory
    63  	transformer       *transformer.Factory
    64  	ui                *ui.Factory
    65  	startedAt         time.Time
    66  	sessions          *cache.SessionCache
    67  	sandboxes         *cache.SandboxCache
    68  	loginOptions      map[string]interface{}
    69  	logger            *zap.Logger
    70  }
    71  
    72  // PortalParameters are input parameters for NewPortal.
    73  type PortalParameters struct {
    74  	Config                *PortalConfig              `json:"config,omitempty" xml:"config,omitempty" yaml:"config,omitempty"`
    75  	Logger                *zap.Logger                `json:"logger,omitempty" xml:"logger,omitempty" yaml:"logger,omitempty"`
    76  	IdentityStores        []ids.IdentityStore        `json:"identity_stores,omitempty" xml:"identity_stores,omitempty" yaml:"identity_stores,omitempty"`
    77  	IdentityProviders     []idp.IdentityProvider     `json:"identity_providers,omitempty" xml:"identity_providers,omitempty" yaml:"identity_providers,omitempty"`
    78  	SingleSignOnProviders []sso.SingleSignOnProvider `json:"sso_providers,omitempty" xml:"sso_providers,omitempty" yaml:"sso_providers,omitempty"`
    79  }
    80  
    81  // NewPortal returns an instance of Portal.
    82  func NewPortal(params PortalParameters) (*Portal, error) {
    83  	if params.Logger == nil {
    84  		return nil, errors.ErrNewPortalLoggerNil
    85  	}
    86  	if params.Config == nil {
    87  		return nil, errors.ErrNewPortalConfigNil
    88  	}
    89  
    90  	if err := params.Config.Validate(); err != nil {
    91  		return nil, errors.ErrNewPortal.WithArgs(err)
    92  	}
    93  	p := &Portal{
    94  		id:     uuid.New().String(),
    95  		config: params.Config,
    96  		logger: params.Logger,
    97  	}
    98  
    99  	for _, storeName := range params.Config.IdentityStores {
   100  		var storeFound bool
   101  		for _, store := range params.IdentityStores {
   102  			if store.GetName() == storeName {
   103  				if !store.Configured() {
   104  					return nil, errors.ErrNewPortal.WithArgs(
   105  						fmt.Errorf("identity store %q not configured", storeName),
   106  					)
   107  				}
   108  				p.identityStores = append(p.identityStores, store)
   109  				storeFound = true
   110  				break
   111  			}
   112  		}
   113  		if !storeFound {
   114  			return nil, errors.ErrNewPortal.WithArgs(
   115  				fmt.Errorf("identity store %q not found", storeName),
   116  			)
   117  		}
   118  	}
   119  
   120  	for _, providerName := range params.Config.IdentityProviders {
   121  		var providerFound bool
   122  		for _, provider := range params.IdentityProviders {
   123  			if provider.GetName() == providerName {
   124  				if !provider.Configured() {
   125  					return nil, errors.ErrNewPortal.WithArgs(
   126  						fmt.Errorf("identity provider %q not configured", providerName),
   127  					)
   128  				}
   129  				p.identityProviders = append(p.identityProviders, provider)
   130  				providerFound = true
   131  				break
   132  			}
   133  		}
   134  		if !providerFound {
   135  			return nil, errors.ErrNewPortal.WithArgs(
   136  				fmt.Errorf("identity provider %q not found", providerName),
   137  			)
   138  		}
   139  	}
   140  
   141  	for _, providerName := range params.Config.SingleSignOnProviders {
   142  		var providerFound bool
   143  		for _, provider := range params.SingleSignOnProviders {
   144  			if provider.GetName() == providerName {
   145  				if !provider.Configured() {
   146  					return nil, errors.ErrNewPortal.WithArgs(
   147  						fmt.Errorf("sso provider %q not configured", providerName),
   148  					)
   149  				}
   150  				p.ssoProviders = append(p.ssoProviders, provider)
   151  				providerFound = true
   152  				break
   153  			}
   154  		}
   155  		if !providerFound {
   156  			return nil, errors.ErrNewPortal.WithArgs(
   157  				fmt.Errorf("sso provider %q not found", providerName),
   158  			)
   159  		}
   160  	}
   161  
   162  	if len(p.identityStores) < 1 && len(p.identityProviders) < 1 {
   163  		return nil, errors.ErrNewPortal.WithArgs(errors.ErrPortalConfigBackendsNotFound)
   164  	}
   165  
   166  	if err := p.configure(); err != nil {
   167  		return nil, err
   168  	}
   169  	return p, nil
   170  }
   171  
   172  // GetName returns the configuration name of the Portal.
   173  func (p *Portal) GetName() string {
   174  	return p.config.Name
   175  }
   176  
   177  func (p *Portal) configure() error {
   178  	if err := p.configureEssentials(); err != nil {
   179  		return err
   180  	}
   181  	if err := p.configureCryptoKeyStore(); err != nil {
   182  		return err
   183  	}
   184  	if err := p.configureLoginOptions(); err != nil {
   185  		return err
   186  	}
   187  	if err := p.configureUserInterface(); err != nil {
   188  		return err
   189  	}
   190  	if err := p.configureUserTransformer(); err != nil {
   191  		return err
   192  	}
   193  
   194  	if len(p.config.TrustedLogoutRedirectURIConfigs) > 0 {
   195  		p.logger.Debug(
   196  			"Logout redirect URI configuration",
   197  			zap.Any("trusted_logout_redirect_uri_configs", p.config.TrustedLogoutRedirectURIConfigs),
   198  		)
   199  	} else {
   200  		p.logger.Debug("Logout redirect URI configuration not present")
   201  	}
   202  
   203  	return nil
   204  }
   205  
   206  func (p *Portal) configureEssentials() error {
   207  	p.logger.Debug(
   208  		"Configuring caching",
   209  		zap.String("portal_name", p.config.Name),
   210  		zap.String("portal_id", p.id),
   211  	)
   212  
   213  	p.sessions = cache.NewSessionCache()
   214  	p.sessions.Run()
   215  	p.sandboxes = cache.NewSandboxCache()
   216  	p.sandboxes.Run()
   217  
   218  	p.logger.Debug(
   219  		"Configuring cookie parameters",
   220  		zap.String("portal_name", p.config.Name),
   221  	)
   222  
   223  	c, err := cookie.NewFactory(p.config.CookieConfig)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	p.cookie = c
   228  	return nil
   229  }
   230  
   231  func (p *Portal) configureCryptoKeyStore() error {
   232  	if len(p.config.AccessListConfigs) == 0 {
   233  		p.config.AccessListConfigs = []*acl.RuleConfiguration{
   234  			{
   235  				// Admin users can access everything.
   236  				Conditions: []string{defaultPortalACLCondition},
   237  				Action:     defaultPortalACLAction,
   238  			},
   239  		}
   240  	}
   241  
   242  	p.logger.Debug(
   243  		"Configuring authentication ACL",
   244  		zap.String("portal_name", p.config.Name),
   245  		zap.String("portal_id", p.id),
   246  		zap.Any("access_list_configs", p.config.AccessListConfigs),
   247  	)
   248  
   249  	if p.config.TokenValidatorOptions == nil {
   250  		p.config.TokenValidatorOptions = options.NewTokenValidatorOptions()
   251  	}
   252  	p.config.TokenValidatorOptions.ValidateBearerHeader = true
   253  
   254  	// The below line is disabled because path match is not part of the ACL.
   255  	// p.config.TokenValidatorOptions.ValidateMethodPath = true
   256  
   257  	accessList := acl.NewAccessList()
   258  	accessList.SetLogger(p.logger)
   259  	ctx := context.Background()
   260  	if err := accessList.AddRules(ctx, p.config.AccessListConfigs); err != nil {
   261  		return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err)
   262  	}
   263  
   264  	p.keystore = kms.NewCryptoKeyStore()
   265  	p.keystore.SetLogger(p.logger)
   266  
   267  	// Load token configuration into key managers, extract token verification
   268  	// keys and add them to token validator.
   269  	if p.config.CryptoKeyStoreConfig != nil {
   270  		// Add default token name, lifetime, etc.
   271  		if err := p.keystore.AddDefaults(p.config.CryptoKeyStoreConfig); err != nil {
   272  			return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err)
   273  		}
   274  	}
   275  
   276  	if len(p.config.CryptoKeyConfigs) == 0 {
   277  		if err := p.keystore.AutoGenerate("default", "ES512"); err != nil {
   278  			return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err)
   279  		}
   280  	} else {
   281  		if err := p.keystore.AddKeysWithConfigs(p.config.CryptoKeyConfigs); err != nil {
   282  			return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err)
   283  		}
   284  	}
   285  
   286  	if err := p.keystore.HasVerifyKeys(); err != nil {
   287  		return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err)
   288  	}
   289  
   290  	p.validator = validator.NewTokenValidator()
   291  	if err := p.validator.Configure(ctx, p.keystore.GetVerifyKeys(), accessList, p.config.TokenValidatorOptions); err != nil {
   292  		return errors.ErrCryptoKeyStoreConfig.WithArgs(p.config.Name, err)
   293  	}
   294  
   295  	p.logger.Debug(
   296  		"Configured validator ACL",
   297  		zap.String("portal_name", p.config.Name),
   298  		zap.String("portal_id", p.id),
   299  		zap.Any("token_validator_options", p.config.TokenValidatorOptions),
   300  		zap.Any("token_grantor_options", p.config.TokenGrantorOptions),
   301  	)
   302  	return nil
   303  }
   304  
   305  func (p *Portal) configureLoginOptions() error {
   306  	p.loginOptions = make(map[string]interface{})
   307  	p.loginOptions["form_required"] = "no"
   308  	p.loginOptions["realm_dropdown_required"] = "no"
   309  	p.loginOptions["authenticators_required"] = "no"
   310  	p.loginOptions["identity_required"] = "no"
   311  
   312  	if err := p.configureIdentityStoreLogin(); err != nil {
   313  		return err
   314  	}
   315  
   316  	if err := p.configureIdentityProviderLogin(); err != nil {
   317  		return err
   318  	}
   319  
   320  	if err := p.configureLoginIcons(); err != nil {
   321  		return err
   322  	}
   323  
   324  	p.logger.Debug(
   325  		"Provisioned login options",
   326  		zap.String("portal_name", p.config.Name),
   327  		zap.String("portal_id", p.id),
   328  		zap.Any("options", p.loginOptions),
   329  		zap.Int("identity_store_count", len(p.config.IdentityStores)),
   330  		zap.Int("identity_provider_count", len(p.config.IdentityProviders)),
   331  	)
   332  
   333  	return nil
   334  }
   335  
   336  func (p *Portal) configureLoginIcons() error {
   337  	var entries []*icons.LoginIcon
   338  
   339  	for _, store := range p.identityStores {
   340  		icon := store.GetLoginIcon()
   341  		entries = append(entries, icon)
   342  	}
   343  
   344  	for _, provider := range p.identityProviders {
   345  		icon := provider.GetLoginIcon()
   346  		entries = append(entries, icon)
   347  	}
   348  
   349  	sort.Slice(entries[:], func(i, j int) bool {
   350  		return entries[i].Priority > entries[j].Priority
   351  	})
   352  
   353  	var iconConfigs []map[string]string
   354  
   355  	for i, icon := range entries {
   356  		iconConfig := icon.GetConfig()
   357  		iconConfigs = append(iconConfigs, iconConfig)
   358  		if i == 0 {
   359  			p.loginOptions["default_realm"] = iconConfig["realm"]
   360  		}
   361  	}
   362  
   363  	p.loginOptions["authenticators"] = iconConfigs
   364  
   365  	if len(iconConfigs) == 1 {
   366  		p.loginOptions["hide_contact_support_link"] = "yes"
   367  		p.loginOptions["hide_forgot_username_link"] = "yes"
   368  		p.loginOptions["hide_register_link"] = "yes"
   369  		p.loginOptions["hide_links"] = "yes"
   370  		for _, iconConfig := range iconConfigs {
   371  			if v, exists := iconConfig["contact_support_enabled"]; exists && v == "yes" {
   372  				p.loginOptions["hide_contact_support_link"] = "no"
   373  				p.loginOptions["hide_links"] = "no"
   374  			}
   375  			if v, exists := iconConfig["registration_enabled"]; exists && v == "yes" {
   376  				p.loginOptions["hide_register_link"] = "no"
   377  				p.loginOptions["hide_links"] = "no"
   378  			}
   379  			if v, exists := iconConfig["username_recovery_enabled"]; exists && v == "yes" {
   380  				p.loginOptions["hide_forgot_username_link"] = "no"
   381  				p.loginOptions["hide_links"] = "no"
   382  			}
   383  		}
   384  	}
   385  
   386  	return nil
   387  }
   388  
   389  func (p *Portal) configureIdentityStoreLogin() error {
   390  	if len(p.config.IdentityStores) < 1 {
   391  		return nil
   392  	}
   393  
   394  	p.logger.Debug(
   395  		"Configuring identity store login options",
   396  		zap.String("portal_name", p.config.Name),
   397  		zap.String("portal_id", p.id),
   398  		zap.Int("identity_store_count", len(p.config.IdentityStores)),
   399  	)
   400  
   401  	var stores []map[string]string
   402  
   403  	for _, store := range p.identityStores {
   404  		cfg := make(map[string]string)
   405  		cfg["realm"] = store.GetRealm()
   406  		cfg["default"] = "no"
   407  		switch store.GetKind() {
   408  		case "local":
   409  			cfg["label"] = strings.ToTitle(store.GetRealm())
   410  			cfg["default"] = "yes"
   411  		case "ldap":
   412  			cfg["label"] = strings.ToUpper(store.GetRealm())
   413  		default:
   414  			cfg["label"] = strings.ToTitle(store.GetRealm())
   415  		}
   416  		stores = append(stores, cfg)
   417  	}
   418  
   419  	if len(stores) > 0 {
   420  		p.loginOptions["form_required"] = "yes"
   421  		p.loginOptions["identity_required"] = "yes"
   422  		p.loginOptions["realms"] = stores
   423  	}
   424  
   425  	if len(stores) > 1 {
   426  		p.loginOptions["realm_dropdown_required"] = "yes"
   427  		p.loginOptions["authenticators_required"] = "yes"
   428  	}
   429  
   430  	for _, store := range p.identityStores {
   431  		icon := store.GetLoginIcon()
   432  		icon.SetRealm(store.GetRealm())
   433  		switch store.GetKind() {
   434  		case "local":
   435  			icon.RegistrationEnabled = false
   436  			icon.UsernameRecoveryEnabled = false
   437  		case "ldap":
   438  			icon.RegistrationEnabled = false
   439  			icon.UsernameRecoveryEnabled = false
   440  		}
   441  	}
   442  
   443  	return nil
   444  }
   445  
   446  func (p *Portal) configureIdentityProviderLogin() error {
   447  	if len(p.config.IdentityProviders) < 1 {
   448  		return nil
   449  	}
   450  
   451  	p.logger.Debug(
   452  		"Configuring identity provider login options",
   453  		zap.String("portal_name", p.config.Name),
   454  		zap.String("portal_id", p.id),
   455  		zap.Int("identity_provider_count", len(p.config.IdentityProviders)),
   456  	)
   457  
   458  	for _, provider := range p.identityProviders {
   459  		icon := provider.GetLoginIcon()
   460  		icon.SetRealm(provider.GetRealm())
   461  		switch provider.GetKind() {
   462  		case "oauth":
   463  			icon.SetEndpoint(path.Join(provider.GetKind()+"2", provider.GetRealm()))
   464  		default:
   465  			icon.SetEndpoint(path.Join(provider.GetKind(), provider.GetRealm()))
   466  		}
   467  	}
   468  
   469  	p.loginOptions["authenticators_required"] = "yes"
   470  
   471  	return nil
   472  }
   473  
   474  func (p *Portal) configureUserInterface() error {
   475  	p.logger.Debug(
   476  		"Configuring user interface",
   477  		zap.String("portal_name", p.config.Name),
   478  		zap.String("portal_id", p.id),
   479  	)
   480  
   481  	p.ui = ui.NewFactory()
   482  	if p.config.UI.Title == "" {
   483  		p.ui.Title = "Sign In"
   484  	} else {
   485  		p.ui.Title = p.config.UI.Title
   486  	}
   487  
   488  	if p.config.UI.CustomCSSPath != "" {
   489  		p.ui.CustomCSSPath = p.config.UI.CustomCSSPath
   490  		if err := ui.StaticAssets.AddAsset("assets/css/custom.css", "text/css", p.config.UI.CustomCSSPath); err != nil {
   491  			return errors.ErrStaticAssetAddFailed.WithArgs("assets/css/custom.css", "text/css", p.config.UI.CustomCSSPath, p.config.Name, err)
   492  		}
   493  	}
   494  
   495  	if p.config.UI.CustomJsPath != "" {
   496  		p.ui.CustomJsPath = p.config.UI.CustomJsPath
   497  		if err := ui.StaticAssets.AddAsset("assets/js/custom.js", "application/javascript", p.config.UI.CustomJsPath); err != nil {
   498  			return errors.ErrStaticAssetAddFailed.WithArgs("assets/js/custom.js", "application/javascript", p.config.UI.CustomJsPath, p.config.Name, err)
   499  		}
   500  	}
   501  
   502  	if p.config.UI.CustomHTMLHeaderPath != "" {
   503  		b, err := os.ReadFile(p.config.UI.CustomHTMLHeaderPath)
   504  		if err != nil {
   505  			return errors.ErrCustomHTMLHeaderNotReadable.WithArgs(p.config.UI.CustomHTMLHeaderPath, p.config.Name, err)
   506  		}
   507  		for k, v := range ui.PageTemplates {
   508  			headIndex := strings.Index(v, "<meta name=\"description\"")
   509  			if headIndex < 1 {
   510  				continue
   511  			}
   512  			v = v[:headIndex] + string(b) + v[headIndex:]
   513  			ui.PageTemplates[k] = v
   514  		}
   515  	}
   516  
   517  	for _, staticAsset := range p.config.UI.StaticAssets {
   518  		if err := ui.StaticAssets.AddAsset(staticAsset.Path, staticAsset.ContentType, staticAsset.FsPath); err != nil {
   519  			return errors.ErrStaticAssetAddFailed.WithArgs(staticAsset.Path, staticAsset.ContentType, staticAsset.FsPath, p.config.Name, err)
   520  		}
   521  	}
   522  
   523  	if p.config.UI.LogoURL != "" {
   524  		p.ui.LogoURL = p.config.UI.LogoURL
   525  		p.ui.LogoDescription = p.config.UI.LogoDescription
   526  	} else {
   527  		p.ui.LogoURL = path.Join(p.ui.LogoURL)
   528  	}
   529  
   530  	if p.config.UI.MetaTitle != "" {
   531  		p.ui.MetaTitle = p.config.UI.MetaTitle
   532  	} else {
   533  		p.ui.MetaTitle = "Authentication Portal"
   534  	}
   535  
   536  	if p.config.UI.MetaAuthor != "" {
   537  		p.ui.MetaAuthor = p.config.UI.MetaAuthor
   538  	} else {
   539  		p.ui.MetaAuthor = "Paul Greenberg github.com/greenpau"
   540  	}
   541  
   542  	if p.config.UI.MetaDescription != "" {
   543  		p.ui.MetaDescription = p.config.UI.MetaDescription
   544  	} else {
   545  		p.ui.MetaDescription = "Performs user authentication."
   546  	}
   547  
   548  	if len(p.config.UI.PrivateLinks) > 0 {
   549  		p.ui.PrivateLinks = p.config.UI.PrivateLinks
   550  	}
   551  
   552  	if len(p.config.UI.Realms) > 0 {
   553  		p.ui.Realms = p.config.UI.Realms
   554  	}
   555  
   556  	if p.config.UI.Theme == "" {
   557  		p.config.UI.Theme = "basic"
   558  	}
   559  	if _, exists := ui.Themes[p.config.UI.Theme]; !exists {
   560  		return errors.ErrUserInterfaceThemeNotFound.WithArgs(p.config.Name, p.config.UI.Theme)
   561  	}
   562  
   563  	// User Interface Templates
   564  	for k := range ui.PageTemplates {
   565  		tmplNameParts := strings.SplitN(k, "/", 2)
   566  		tmplTheme := tmplNameParts[0]
   567  		tmplName := tmplNameParts[1]
   568  		if tmplTheme != p.config.UI.Theme {
   569  			continue
   570  		}
   571  		if _, exists := p.config.UI.Templates[tmplName]; !exists {
   572  			p.logger.Debug(
   573  				"Configuring default authentication user interface templates",
   574  				zap.String("portal_name", p.config.Name),
   575  				zap.String("template_theme", tmplTheme),
   576  				zap.String("template_name", tmplName),
   577  			)
   578  			if err := p.ui.AddBuiltinTemplate(k); err != nil {
   579  				return errors.ErrUserInterfaceBuiltinTemplateAddFailed.WithArgs(p.config.Name, tmplName, tmplTheme, err)
   580  			}
   581  			p.ui.Templates[tmplName] = p.ui.Templates[k]
   582  		}
   583  	}
   584  
   585  	for tmplName, tmplPath := range p.config.UI.Templates {
   586  		p.logger.Debug(
   587  			"Configuring non-default authentication user interface templates",
   588  			zap.String("portal_name", p.config.Name),
   589  			zap.String("portal_id", p.id),
   590  			zap.String("template_name", tmplName),
   591  			zap.String("template_path", tmplPath),
   592  		)
   593  		if err := p.ui.AddTemplate(tmplName, tmplPath); err != nil {
   594  			return errors.ErrUserInterfaceCustomTemplateAddFailed.WithArgs(p.config.Name, tmplName, tmplPath, err)
   595  		}
   596  	}
   597  
   598  	p.logger.Debug(
   599  		"Configured user interface",
   600  		zap.String("portal_name", p.config.Name),
   601  		zap.String("portal_id", p.id),
   602  		zap.String("title", p.ui.Title),
   603  		zap.String("logo_url", p.ui.LogoURL),
   604  		zap.String("logo_description", p.ui.LogoDescription),
   605  		zap.Any("action_endpoint", p.ui.ActionEndpoint),
   606  		zap.Any("private_links", p.ui.PrivateLinks),
   607  		zap.Any("realms", p.ui.Realms),
   608  		zap.String("theme", p.config.UI.Theme),
   609  	)
   610  
   611  	return nil
   612  }
   613  
   614  func (p *Portal) configureUserTransformer() error {
   615  	if len(p.config.UserTransformerConfigs) == 0 {
   616  		return nil
   617  	}
   618  
   619  	p.logger.Debug(
   620  		"Configuring user transforms",
   621  		zap.String("portal_name", p.config.Name),
   622  		zap.String("portal_id", p.id),
   623  	)
   624  
   625  	tr, err := transformer.NewFactory(p.config.UserTransformerConfigs)
   626  	if err != nil {
   627  		return err
   628  	}
   629  	p.transformer = tr
   630  
   631  	p.logger.Debug(
   632  		"Configured user transforms",
   633  		zap.String("portal_name", p.config.Name),
   634  		zap.String("portal_id", p.id),
   635  		zap.Any("transforms", p.config.UserTransformerConfigs),
   636  	)
   637  	return nil
   638  }
   639  
   640  // AddUserRegistry adds registry.UserRegistry instance to Portal.
   641  func (p *Portal) AddUserRegistry(userRegistry registry.UserRegistry) error {
   642  	p.config.UserRegistries = cfgutil.DedupStrArr(p.config.UserRegistries)
   643  
   644  	if len(p.config.UserRegistries) < 1 {
   645  		return fmt.Errorf("auth portal has no user registries configured")
   646  	}
   647  	if len(p.config.UserRegistries) > 1 {
   648  		return fmt.Errorf("auth portal does not support multiple user registries: %v", p.config.UserRegistries)
   649  	}
   650  
   651  	p.userRegistry = userRegistry
   652  
   653  	p.logger.Debug(
   654  		"Configured user registration",
   655  		zap.String("portal_name", p.config.Name),
   656  		zap.String("portal_id", p.id),
   657  		zap.Any("user_registry", p.userRegistry.GetConfig()),
   658  	)
   659  
   660  	return nil
   661  }
   662  
   663  // GetIdentityStoreNames returns a list of existing identity stores.
   664  func (p *Portal) GetIdentityStoreNames() map[string]string {
   665  	var m map[string]string
   666  	for _, store := range p.identityStores {
   667  		if m == nil {
   668  			m = make(map[string]string)
   669  		}
   670  		m[store.GetName()] = store.GetRealm()
   671  	}
   672  	return m
   673  }