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