github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/identity/openid/openid.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package openid
    19  
    20  import (
    21  	"crypto/sha1"
    22  	"encoding/base64"
    23  	"errors"
    24  	"io"
    25  	"net/http"
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/minio/madmin-go/v3"
    33  	"github.com/minio/minio-go/v7/pkg/set"
    34  	"github.com/minio/minio/internal/arn"
    35  	"github.com/minio/minio/internal/auth"
    36  	"github.com/minio/minio/internal/config"
    37  	"github.com/minio/minio/internal/config/identity/openid/provider"
    38  	"github.com/minio/minio/internal/hash/sha256"
    39  	"github.com/minio/pkg/v2/env"
    40  	xnet "github.com/minio/pkg/v2/net"
    41  	"github.com/minio/pkg/v2/policy"
    42  )
    43  
    44  // OpenID keys and envs.
    45  const (
    46  	ClientID      = "client_id"
    47  	ClientSecret  = "client_secret"
    48  	ConfigURL     = "config_url"
    49  	ClaimName     = "claim_name"
    50  	ClaimUserinfo = "claim_userinfo"
    51  	RolePolicy    = "role_policy"
    52  	DisplayName   = "display_name"
    53  
    54  	Scopes             = "scopes"
    55  	RedirectURI        = "redirect_uri"
    56  	RedirectURIDynamic = "redirect_uri_dynamic"
    57  	Vendor             = "vendor"
    58  
    59  	// Vendor specific ENV only enabled if the Vendor matches == "vendor"
    60  	KeyCloakRealm    = "keycloak_realm"
    61  	KeyCloakAdminURL = "keycloak_admin_url"
    62  
    63  	// Removed params
    64  	JwksURL     = "jwks_url"
    65  	ClaimPrefix = "claim_prefix"
    66  )
    67  
    68  // DefaultKVS - default config for OpenID config
    69  var (
    70  	DefaultKVS = config.KVS{
    71  		config.KV{
    72  			Key:   config.Enable,
    73  			Value: "",
    74  		},
    75  		config.KV{
    76  			Key:   DisplayName,
    77  			Value: "",
    78  		},
    79  		config.KV{
    80  			Key:   ConfigURL,
    81  			Value: "",
    82  		},
    83  		config.KV{
    84  			Key:   ClientID,
    85  			Value: "",
    86  		},
    87  		config.KV{
    88  			Key:   ClientSecret,
    89  			Value: "",
    90  		},
    91  		config.KV{
    92  			Key:   ClaimName,
    93  			Value: policy.PolicyName,
    94  		},
    95  		config.KV{
    96  			Key:   ClaimUserinfo,
    97  			Value: "",
    98  		},
    99  		config.KV{
   100  			Key:   RolePolicy,
   101  			Value: "",
   102  		},
   103  		config.KV{
   104  			Key:   ClaimPrefix,
   105  			Value: "",
   106  		},
   107  		config.KV{
   108  			Key:   RedirectURI,
   109  			Value: "",
   110  		},
   111  		config.KV{
   112  			Key:   RedirectURIDynamic,
   113  			Value: "off",
   114  		},
   115  		config.KV{
   116  			Key:   Scopes,
   117  			Value: "",
   118  		},
   119  		config.KV{
   120  			Key:   Vendor,
   121  			Value: "",
   122  		},
   123  		config.KV{
   124  			Key:   KeyCloakRealm,
   125  			Value: "",
   126  		},
   127  		config.KV{
   128  			Key:   KeyCloakAdminURL,
   129  			Value: "",
   130  		},
   131  	}
   132  )
   133  
   134  var errSingleProvider = config.Errorf("Only one OpenID provider can be configured if not using role policy mapping")
   135  
   136  // DummyRoleARN is used to indicate that the user associated with it was
   137  // authenticated via policy-claim based OpenID provider.
   138  var DummyRoleARN = func() arn.ARN {
   139  	v, err := arn.NewIAMRoleARN("dummy-internal", "")
   140  	if err != nil {
   141  		panic("should not happen!")
   142  	}
   143  	return v
   144  }()
   145  
   146  // Config - OpenID Config
   147  type Config struct {
   148  	Enabled bool
   149  
   150  	// map of roleARN to providerCfg's
   151  	arnProviderCfgsMap map[arn.ARN]*providerCfg
   152  
   153  	// map of config names to providerCfg's
   154  	ProviderCfgs map[string]*providerCfg
   155  
   156  	pubKeys          publicKeys
   157  	roleArnPolicyMap map[arn.ARN]string
   158  
   159  	transport   http.RoundTripper
   160  	closeRespFn func(io.ReadCloser)
   161  }
   162  
   163  // Clone returns a cloned copy of OpenID config.
   164  func (r *Config) Clone() Config {
   165  	if r == nil {
   166  		return Config{}
   167  	}
   168  	cfg := Config{
   169  		Enabled:            r.Enabled,
   170  		arnProviderCfgsMap: make(map[arn.ARN]*providerCfg, len(r.arnProviderCfgsMap)),
   171  		ProviderCfgs:       make(map[string]*providerCfg, len(r.ProviderCfgs)),
   172  		pubKeys:            r.pubKeys,
   173  		roleArnPolicyMap:   make(map[arn.ARN]string, len(r.roleArnPolicyMap)),
   174  		transport:          r.transport,
   175  		closeRespFn:        r.closeRespFn,
   176  	}
   177  	for k, v := range r.arnProviderCfgsMap {
   178  		cfg.arnProviderCfgsMap[k] = v
   179  	}
   180  	for k, v := range r.ProviderCfgs {
   181  		cfg.ProviderCfgs[k] = v
   182  	}
   183  	for k, v := range r.roleArnPolicyMap {
   184  		cfg.roleArnPolicyMap[k] = v
   185  	}
   186  	return cfg
   187  }
   188  
   189  // LookupConfig lookup jwks from config, override with any ENVs.
   190  func LookupConfig(s config.Config, transport http.RoundTripper, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) {
   191  	openIDClientTransport := http.DefaultTransport
   192  	if transport != nil {
   193  		openIDClientTransport = transport
   194  	}
   195  	c = Config{
   196  		Enabled:            false,
   197  		arnProviderCfgsMap: map[arn.ARN]*providerCfg{},
   198  		ProviderCfgs:       map[string]*providerCfg{},
   199  		pubKeys: publicKeys{
   200  			RWMutex: &sync.RWMutex{},
   201  			pkMap:   map[string]interface{}{},
   202  		},
   203  		roleArnPolicyMap: map[arn.ARN]string{},
   204  		transport:        openIDClientTransport,
   205  		closeRespFn:      closeRespFn,
   206  	}
   207  
   208  	seenClientIDs := set.NewStringSet()
   209  
   210  	deprecatedKeys := []string{JwksURL}
   211  
   212  	// remove this since we have removed support for this already.
   213  	for k := range s[config.IdentityOpenIDSubSys] {
   214  		for _, dk := range deprecatedKeys {
   215  			kvs := s[config.IdentityOpenIDSubSys][k]
   216  			kvs.Delete(dk)
   217  			s[config.IdentityOpenIDSubSys][k] = kvs
   218  		}
   219  	}
   220  
   221  	if err := s.CheckValidKeys(config.IdentityOpenIDSubSys, deprecatedKeys); err != nil {
   222  		return c, err
   223  	}
   224  
   225  	openIDTargets, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
   226  	if err != nil {
   227  		return c, err
   228  	}
   229  
   230  	for _, cfgName := range openIDTargets {
   231  		getCfgVal := func(cfgParam string) string {
   232  			// As parameters are already validated, we skip checking
   233  			// if the config param was found.
   234  			val, _, _ := s.ResolveConfigParam(config.IdentityOpenIDSubSys, cfgName, cfgParam, false)
   235  			return val
   236  		}
   237  
   238  		// In the past, when only one openID provider was allowed, there
   239  		// was no `enable` parameter - the configuration is turned off
   240  		// by clearing the values. With multiple providers, we support
   241  		// individually enabling/disabling provider configurations. If
   242  		// the enable parameter's value is non-empty, we use that
   243  		// setting, otherwise we treat it as enabled if some important
   244  		// parameters are non-empty.
   245  		var (
   246  			cfgEnableVal        = getCfgVal(config.Enable)
   247  			isExplicitlyEnabled = cfgEnableVal != ""
   248  		)
   249  
   250  		var enabled bool
   251  		if isExplicitlyEnabled {
   252  			enabled, err = config.ParseBool(cfgEnableVal)
   253  			if err != nil {
   254  				return c, err
   255  			}
   256  			// No need to continue loading if the config is not enabled.
   257  			if !enabled {
   258  				continue
   259  			}
   260  		}
   261  
   262  		p := newProviderCfgFromConfig(getCfgVal)
   263  		configURL := getCfgVal(ConfigURL)
   264  
   265  		if !isExplicitlyEnabled {
   266  			enabled = true
   267  			if p.ClientID == "" && p.ClientSecret == "" && configURL == "" {
   268  				enabled = false
   269  			}
   270  		}
   271  
   272  		// No need to continue loading if the config is not enabled.
   273  		if !enabled {
   274  			continue
   275  		}
   276  
   277  		// Validate that client ID has not been duplicately specified.
   278  		if seenClientIDs.Contains(p.ClientID) {
   279  			return c, config.Errorf("Client ID %s is present with multiple OpenID configurations", p.ClientID)
   280  		}
   281  		seenClientIDs.Add(p.ClientID)
   282  
   283  		p.URL, err = xnet.ParseHTTPURL(configURL)
   284  		if err != nil {
   285  			return c, err
   286  		}
   287  		configURLDomain := p.URL.Hostname()
   288  		p.DiscoveryDoc, err = parseDiscoveryDoc(p.URL, transport, closeRespFn)
   289  		if err != nil {
   290  			return c, err
   291  		}
   292  
   293  		if p.ClaimUserinfo && configURL == "" {
   294  			return c, errors.New("please specify config_url to enable fetching claims from UserInfo endpoint")
   295  		}
   296  
   297  		if scopeList := getCfgVal(Scopes); scopeList != "" {
   298  			var scopes []string
   299  			for _, scope := range strings.Split(scopeList, ",") {
   300  				scope = strings.TrimSpace(scope)
   301  				if scope == "" {
   302  					return c, config.Errorf("empty scope value is not allowed '%s', please refer to our documentation", scopeList)
   303  				}
   304  				scopes = append(scopes, scope)
   305  			}
   306  			// Replace the discovery document scopes by client customized scopes.
   307  			p.DiscoveryDoc.ScopesSupported = scopes
   308  		}
   309  
   310  		// Check if claim name is the non-default value and role policy is set.
   311  		if p.ClaimName != policy.PolicyName && p.RolePolicy != "" {
   312  			// In the unlikely event that the user specifies
   313  			// `policy.PolicyName` as the claim name explicitly and sets
   314  			// a role policy, this check is thwarted, but we will be using
   315  			// the role policy anyway.
   316  			return c, config.Errorf("Role Policy (=`%s`) and Claim Name (=`%s`) cannot both be set", p.RolePolicy, p.ClaimName)
   317  		}
   318  
   319  		jwksURL := p.DiscoveryDoc.JwksURI
   320  		if jwksURL == "" {
   321  			return c, config.Errorf("no JWKS URI found in your provider's discovery doc (config_url=%s)", configURL)
   322  		}
   323  
   324  		p.JWKS.URL, err = xnet.ParseHTTPURL(jwksURL)
   325  		if err != nil {
   326  			return c, err
   327  		}
   328  
   329  		if p.RolePolicy != "" {
   330  			// RolePolicy is validated by IAM System during its
   331  			// initialization.
   332  
   333  			// Generate role ARN as combination of provider domain and
   334  			// prefix of client ID.
   335  			domain := configURLDomain
   336  			if domain == "" {
   337  				// Attempt to parse the JWKs URI.
   338  				domain = p.JWKS.URL.Hostname()
   339  				if domain == "" {
   340  					return c, config.Errorf("unable to parse a domain from the OpenID config")
   341  				}
   342  			}
   343  			if p.ClientID == "" {
   344  				return c, config.Errorf("client ID must not be empty")
   345  			}
   346  
   347  			// We set the resource ID of the role arn as a hash of client
   348  			// ID, so we can get a short roleARN that stays the same on
   349  			// restart.
   350  			var resourceID string
   351  			{
   352  				h := sha1.New()
   353  				h.Write([]byte(p.ClientID))
   354  				bs := h.Sum(nil)
   355  				resourceID = base64.RawURLEncoding.EncodeToString(bs)
   356  			}
   357  			p.roleArn, err = arn.NewIAMRoleARN(resourceID, serverRegion)
   358  			if err != nil {
   359  				return c, config.Errorf("unable to generate ARN from the OpenID config: %v", err)
   360  			}
   361  
   362  			c.roleArnPolicyMap[p.roleArn] = p.RolePolicy
   363  		} else if p.ClaimName == "" {
   364  			return c, config.Errorf("A role policy or claim name must be specified")
   365  		}
   366  
   367  		if err = p.initializeProvider(getCfgVal, c.transport); err != nil {
   368  			return c, err
   369  		}
   370  
   371  		arnKey := p.roleArn
   372  		if p.RolePolicy == "" {
   373  			arnKey = DummyRoleARN
   374  			// Ensure that at most one JWT policy claim based provider may be
   375  			// defined.
   376  			if _, ok := c.arnProviderCfgsMap[DummyRoleARN]; ok {
   377  				return c, errSingleProvider
   378  			}
   379  		}
   380  
   381  		c.arnProviderCfgsMap[arnKey] = &p
   382  		c.ProviderCfgs[cfgName] = &p
   383  
   384  		if err = c.PopulatePublicKey(arnKey); err != nil {
   385  			return c, err
   386  		}
   387  	}
   388  
   389  	c.Enabled = true
   390  
   391  	return c, nil
   392  }
   393  
   394  // ErrProviderConfigNotFound - represents a non-existing provider error.
   395  var ErrProviderConfigNotFound = errors.New("provider configuration not found")
   396  
   397  // GetConfigInfo - returns configuration and related info for the given IDP
   398  // provider.
   399  func (r *Config) GetConfigInfo(s config.Config, cfgName string) ([]madmin.IDPCfgInfo, error) {
   400  	openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  
   405  	present := false
   406  	for _, cfg := range openIDConfigs {
   407  		if cfg == cfgName {
   408  			present = true
   409  			break
   410  		}
   411  	}
   412  
   413  	if !present {
   414  		return nil, ErrProviderConfigNotFound
   415  	}
   416  
   417  	kvsrcs, err := s.GetResolvedConfigParams(config.IdentityOpenIDSubSys, cfgName, true)
   418  	if err != nil {
   419  		return nil, err
   420  	}
   421  
   422  	res := make([]madmin.IDPCfgInfo, 0, len(kvsrcs)+1)
   423  	for _, kvsrc := range kvsrcs {
   424  		// skip returning default config values.
   425  		if kvsrc.Src == config.ValueSourceDef {
   426  			if kvsrc.Key != madmin.EnableKey {
   427  				continue
   428  			}
   429  			// for EnableKey we set an explicit on/off from live configuration
   430  			// if it is present.
   431  			if _, ok := r.ProviderCfgs[cfgName]; !ok {
   432  				// No live config is present
   433  				continue
   434  			}
   435  			if r.Enabled {
   436  				kvsrc.Value = "on"
   437  			} else {
   438  				kvsrc.Value = "off"
   439  			}
   440  		}
   441  		res = append(res, madmin.IDPCfgInfo{
   442  			Key:   kvsrc.Key,
   443  			Value: kvsrc.Value,
   444  			IsCfg: true,
   445  			IsEnv: kvsrc.Src == config.ValueSourceEnv,
   446  		})
   447  	}
   448  
   449  	if provCfg, exists := r.ProviderCfgs[cfgName]; exists && provCfg.RolePolicy != "" {
   450  		// Append roleARN
   451  		res = append(res, madmin.IDPCfgInfo{
   452  			Key:   "roleARN",
   453  			Value: provCfg.roleArn.String(),
   454  			IsCfg: false,
   455  		})
   456  	}
   457  
   458  	// sort the structs by the key
   459  	sort.Slice(res, func(i, j int) bool {
   460  		return res[i].Key < res[j].Key
   461  	})
   462  
   463  	return res, nil
   464  }
   465  
   466  // GetConfigList - list openID configurations
   467  func (r *Config) GetConfigList(s config.Config) ([]madmin.IDPListItem, error) {
   468  	openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	var res []madmin.IDPListItem
   474  	for _, cfg := range openIDConfigs {
   475  		pcfg, ok := r.ProviderCfgs[cfg]
   476  		if !ok {
   477  			res = append(res, madmin.IDPListItem{
   478  				Type:    "openid",
   479  				Name:    cfg,
   480  				Enabled: false,
   481  			})
   482  		} else {
   483  			var roleARN string
   484  			if pcfg.RolePolicy != "" {
   485  				roleARN = pcfg.roleArn.String()
   486  			}
   487  			res = append(res, madmin.IDPListItem{
   488  				Type:    "openid",
   489  				Name:    cfg,
   490  				Enabled: r.Enabled,
   491  				RoleARN: roleARN,
   492  			})
   493  		}
   494  	}
   495  
   496  	return res, nil
   497  }
   498  
   499  // Enabled returns if configURL is enabled.
   500  func Enabled(kvs config.KVS) bool {
   501  	return kvs.Get(ConfigURL) != ""
   502  }
   503  
   504  // GetSettings - fetches OIDC settings for site-replication related validation.
   505  // NOTE that region must be populated by caller as this package does not know.
   506  func (r *Config) GetSettings() madmin.OpenIDSettings {
   507  	res := madmin.OpenIDSettings{}
   508  	if !r.Enabled {
   509  		return res
   510  	}
   511  	h := sha256.New()
   512  	for arn, provCfg := range r.arnProviderCfgsMap {
   513  		hashedSecret := ""
   514  		{
   515  			h.Reset()
   516  			h.Write([]byte(provCfg.ClientSecret))
   517  			hashedSecret = base64.RawURLEncoding.EncodeToString(h.Sum(nil))
   518  		}
   519  		if arn != DummyRoleARN {
   520  			if res.Roles == nil {
   521  				res.Roles = make(map[string]madmin.OpenIDProviderSettings)
   522  			}
   523  			res.Roles[arn.String()] = madmin.OpenIDProviderSettings{
   524  				ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
   525  				RolePolicy:           provCfg.RolePolicy,
   526  				ClientID:             provCfg.ClientID,
   527  				HashedClientSecret:   hashedSecret,
   528  			}
   529  		} else {
   530  			res.ClaimProvider = madmin.OpenIDProviderSettings{
   531  				ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
   532  				RolePolicy:           provCfg.RolePolicy,
   533  				ClientID:             provCfg.ClientID,
   534  				HashedClientSecret:   hashedSecret,
   535  			}
   536  		}
   537  
   538  	}
   539  
   540  	return res
   541  }
   542  
   543  // GetIAMPolicyClaimName - returns the policy claim name for the (at most one)
   544  // provider configured without a role policy.
   545  func (r *Config) GetIAMPolicyClaimName() string {
   546  	pCfg, ok := r.arnProviderCfgsMap[DummyRoleARN]
   547  	if !ok {
   548  		return ""
   549  	}
   550  	return pCfg.ClaimPrefix + pCfg.ClaimName
   551  }
   552  
   553  // LookupUser lookup userid for the provider
   554  func (r Config) LookupUser(roleArn, userid string) (provider.User, error) {
   555  	// Can safely ignore error here as empty or invalid ARNs will not be
   556  	// mapped.
   557  	arnVal, _ := arn.Parse(roleArn)
   558  	pCfg, ok := r.arnProviderCfgsMap[arnVal]
   559  	if ok {
   560  		user, err := pCfg.provider.LookupUser(userid)
   561  		if err != nil && err != provider.ErrAccessTokenExpired {
   562  			return user, err
   563  		}
   564  		if err == provider.ErrAccessTokenExpired {
   565  			if err = pCfg.provider.LoginWithClientID(pCfg.ClientID, pCfg.ClientSecret); err != nil {
   566  				return user, err
   567  			}
   568  			user, err = pCfg.provider.LookupUser(userid)
   569  		}
   570  		return user, err
   571  	}
   572  	// Without any specific logic for a provider, all accounts
   573  	// are always enabled.
   574  	return provider.User{ID: userid, Enabled: true}, nil
   575  }
   576  
   577  // ProviderEnabled returns true if any vendor specific provider is enabled.
   578  func (r Config) ProviderEnabled() bool {
   579  	if !r.Enabled {
   580  		return false
   581  	}
   582  	for _, v := range r.arnProviderCfgsMap {
   583  		if v.provider != nil {
   584  			return true
   585  		}
   586  	}
   587  	return false
   588  }
   589  
   590  // GetRoleInfo - returns ARN to policies map if a role policy based openID
   591  // provider is configured. Otherwise returns nil.
   592  func (r Config) GetRoleInfo() map[arn.ARN]string {
   593  	for _, p := range r.arnProviderCfgsMap {
   594  		if p.RolePolicy != "" {
   595  			return r.roleArnPolicyMap
   596  		}
   597  	}
   598  	return nil
   599  }
   600  
   601  // GetDefaultExpiration - returns the expiration seconds expected.
   602  func GetDefaultExpiration(dsecs string) (time.Duration, error) {
   603  	timeout := env.Get(config.EnvMinioStsDuration, "")
   604  	defaultExpiryDuration, err := time.ParseDuration(timeout)
   605  	if err != nil {
   606  		defaultExpiryDuration = time.Hour
   607  	}
   608  	if timeout == "" && dsecs != "" {
   609  		expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
   610  		if err != nil {
   611  			return 0, auth.ErrInvalidDuration
   612  		}
   613  
   614  		// The duration, in seconds, of the role session.
   615  		// The value can range from 900 seconds (15 minutes)
   616  		// up to 365 days.
   617  		if expirySecs < config.MinExpiration || expirySecs > config.MaxExpiration {
   618  			return 0, auth.ErrInvalidDuration
   619  		}
   620  
   621  		defaultExpiryDuration = time.Duration(expirySecs) * time.Second
   622  	} else if timeout == "" && dsecs == "" {
   623  		return time.Hour, nil
   624  	}
   625  
   626  	if defaultExpiryDuration.Seconds() < config.MinExpiration || defaultExpiryDuration.Seconds() > config.MaxExpiration {
   627  		return 0, auth.ErrInvalidDuration
   628  	}
   629  
   630  	return defaultExpiryDuration, nil
   631  }