mirror of
https://github.com/minio/minio.git
synced 2024-12-29 00:23:21 -05:00
f3248a4b37
This change adds a `Secret` property to `HelpKV` to identify secrets like passwords and auth tokens that should not be revealed by the server in its configuration fetching APIs. Configuration reporting APIs now do not return secrets.
619 lines
16 KiB
Go
619 lines
16 KiB
Go
// Copyright (c) 2015-2022 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package openid
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/minio/madmin-go/v3"
|
|
"github.com/minio/minio-go/v7/pkg/set"
|
|
"github.com/minio/minio/internal/arn"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/minio/internal/config"
|
|
"github.com/minio/minio/internal/config/identity/openid/provider"
|
|
"github.com/minio/minio/internal/hash/sha256"
|
|
iampolicy "github.com/minio/pkg/iam/policy"
|
|
xnet "github.com/minio/pkg/net"
|
|
)
|
|
|
|
// OpenID keys and envs.
|
|
const (
|
|
ClientID = "client_id"
|
|
ClientSecret = "client_secret"
|
|
ConfigURL = "config_url"
|
|
ClaimName = "claim_name"
|
|
ClaimUserinfo = "claim_userinfo"
|
|
RolePolicy = "role_policy"
|
|
DisplayName = "display_name"
|
|
|
|
Scopes = "scopes"
|
|
RedirectURI = "redirect_uri"
|
|
RedirectURIDynamic = "redirect_uri_dynamic"
|
|
Vendor = "vendor"
|
|
|
|
// Vendor specific ENV only enabled if the Vendor matches == "vendor"
|
|
KeyCloakRealm = "keycloak_realm"
|
|
KeyCloakAdminURL = "keycloak_admin_url"
|
|
|
|
// Removed params
|
|
JwksURL = "jwks_url"
|
|
ClaimPrefix = "claim_prefix"
|
|
)
|
|
|
|
// DefaultKVS - default config for OpenID config
|
|
var (
|
|
DefaultKVS = config.KVS{
|
|
config.KV{
|
|
Key: config.Enable,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: DisplayName,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: ConfigURL,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: ClientID,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: ClientSecret,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: ClaimName,
|
|
Value: iampolicy.PolicyName,
|
|
},
|
|
config.KV{
|
|
Key: ClaimUserinfo,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: RolePolicy,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: ClaimPrefix,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: RedirectURI,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: RedirectURIDynamic,
|
|
Value: "off",
|
|
},
|
|
config.KV{
|
|
Key: Scopes,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: Vendor,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: KeyCloakRealm,
|
|
Value: "",
|
|
},
|
|
config.KV{
|
|
Key: KeyCloakAdminURL,
|
|
Value: "",
|
|
},
|
|
}
|
|
)
|
|
|
|
var errSingleProvider = config.Errorf("Only one OpenID provider can be configured if not using role policy mapping")
|
|
|
|
// DummyRoleARN is used to indicate that the user associated with it was
|
|
// authenticated via policy-claim based OpenID provider.
|
|
var DummyRoleARN = func() arn.ARN {
|
|
v, err := arn.NewIAMRoleARN("dummy-internal", "")
|
|
if err != nil {
|
|
panic("should not happen!")
|
|
}
|
|
return v
|
|
}()
|
|
|
|
// Config - OpenID Config
|
|
type Config struct {
|
|
Enabled bool
|
|
|
|
// map of roleARN to providerCfg's
|
|
arnProviderCfgsMap map[arn.ARN]*providerCfg
|
|
|
|
// map of config names to providerCfg's
|
|
ProviderCfgs map[string]*providerCfg
|
|
|
|
pubKeys publicKeys
|
|
roleArnPolicyMap map[arn.ARN]string
|
|
|
|
transport http.RoundTripper
|
|
closeRespFn func(io.ReadCloser)
|
|
}
|
|
|
|
// Clone returns a cloned copy of OpenID config.
|
|
func (r *Config) Clone() Config {
|
|
if r == nil {
|
|
return Config{}
|
|
}
|
|
cfg := Config{
|
|
Enabled: r.Enabled,
|
|
arnProviderCfgsMap: make(map[arn.ARN]*providerCfg, len(r.arnProviderCfgsMap)),
|
|
ProviderCfgs: make(map[string]*providerCfg, len(r.ProviderCfgs)),
|
|
pubKeys: r.pubKeys,
|
|
roleArnPolicyMap: make(map[arn.ARN]string, len(r.roleArnPolicyMap)),
|
|
transport: r.transport,
|
|
closeRespFn: r.closeRespFn,
|
|
}
|
|
for k, v := range r.arnProviderCfgsMap {
|
|
cfg.arnProviderCfgsMap[k] = v
|
|
}
|
|
for k, v := range r.ProviderCfgs {
|
|
cfg.ProviderCfgs[k] = v
|
|
}
|
|
for k, v := range r.roleArnPolicyMap {
|
|
cfg.roleArnPolicyMap[k] = v
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// LookupConfig lookup jwks from config, override with any ENVs.
|
|
func LookupConfig(s config.Config, transport http.RoundTripper, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) {
|
|
openIDClientTransport := http.DefaultTransport
|
|
if transport != nil {
|
|
openIDClientTransport = transport
|
|
}
|
|
c = Config{
|
|
Enabled: false,
|
|
arnProviderCfgsMap: map[arn.ARN]*providerCfg{},
|
|
ProviderCfgs: map[string]*providerCfg{},
|
|
pubKeys: publicKeys{
|
|
RWMutex: &sync.RWMutex{},
|
|
pkMap: map[string]interface{}{},
|
|
},
|
|
roleArnPolicyMap: map[arn.ARN]string{},
|
|
transport: openIDClientTransport,
|
|
closeRespFn: closeRespFn,
|
|
}
|
|
|
|
seenClientIDs := set.NewStringSet()
|
|
|
|
deprecatedKeys := []string{JwksURL}
|
|
|
|
// remove this since we have removed support for this already.
|
|
for k := range s[config.IdentityOpenIDSubSys] {
|
|
for _, dk := range deprecatedKeys {
|
|
kvs := s[config.IdentityOpenIDSubSys][k]
|
|
kvs.Delete(dk)
|
|
s[config.IdentityOpenIDSubSys][k] = kvs
|
|
}
|
|
}
|
|
|
|
if err := s.CheckValidKeys(config.IdentityOpenIDSubSys, deprecatedKeys); err != nil {
|
|
return c, err
|
|
}
|
|
|
|
openIDTargets, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
|
|
for _, cfgName := range openIDTargets {
|
|
getCfgVal := func(cfgParam string) string {
|
|
// As parameters are already validated, we skip checking
|
|
// if the config param was found.
|
|
val, _, _ := s.ResolveConfigParam(config.IdentityOpenIDSubSys, cfgName, cfgParam, false)
|
|
return val
|
|
}
|
|
|
|
// In the past, when only one openID provider was allowed, there
|
|
// was no `enable` parameter - the configuration is turned off
|
|
// by clearing the values. With multiple providers, we support
|
|
// individually enabling/disabling provider configurations. If
|
|
// the enable parameter's value is non-empty, we use that
|
|
// setting, otherwise we treat it as enabled if some important
|
|
// parameters are non-empty.
|
|
var (
|
|
cfgEnableVal = getCfgVal(config.Enable)
|
|
isExplicitlyEnabled = false
|
|
)
|
|
if cfgEnableVal != "" {
|
|
isExplicitlyEnabled = true
|
|
}
|
|
|
|
var enabled bool
|
|
if isExplicitlyEnabled {
|
|
enabled, err = config.ParseBool(cfgEnableVal)
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
// No need to continue loading if the config is not enabled.
|
|
if !enabled {
|
|
continue
|
|
}
|
|
}
|
|
|
|
p := newProviderCfgFromConfig(getCfgVal)
|
|
configURL := getCfgVal(ConfigURL)
|
|
|
|
if !isExplicitlyEnabled {
|
|
enabled = true
|
|
if p.ClientID == "" && p.ClientSecret == "" && configURL == "" {
|
|
enabled = false
|
|
}
|
|
}
|
|
|
|
// No need to continue loading if the config is not enabled.
|
|
if !enabled {
|
|
continue
|
|
}
|
|
|
|
// Validate that client ID has not been duplicately specified.
|
|
if seenClientIDs.Contains(p.ClientID) {
|
|
return c, config.Errorf("Client ID %s is present with multiple OpenID configurations", p.ClientID)
|
|
}
|
|
seenClientIDs.Add(p.ClientID)
|
|
|
|
p.URL, err = xnet.ParseHTTPURL(configURL)
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
configURLDomain := p.URL.Hostname()
|
|
p.DiscoveryDoc, err = parseDiscoveryDoc(p.URL, transport, closeRespFn)
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
|
|
if p.ClaimUserinfo && configURL == "" {
|
|
return c, errors.New("please specify config_url to enable fetching claims from UserInfo endpoint")
|
|
}
|
|
|
|
if scopeList := getCfgVal(Scopes); scopeList != "" {
|
|
var scopes []string
|
|
for _, scope := range strings.Split(scopeList, ",") {
|
|
scope = strings.TrimSpace(scope)
|
|
if scope == "" {
|
|
return c, config.Errorf("empty scope value is not allowed '%s', please refer to our documentation", scopeList)
|
|
}
|
|
scopes = append(scopes, scope)
|
|
}
|
|
// Replace the discovery document scopes by client customized scopes.
|
|
p.DiscoveryDoc.ScopesSupported = scopes
|
|
}
|
|
|
|
// Check if claim name is the non-default value and role policy is set.
|
|
if p.ClaimName != iampolicy.PolicyName && p.RolePolicy != "" {
|
|
// In the unlikely event that the user specifies
|
|
// `iampolicy.PolicyName` as the claim name explicitly and sets
|
|
// a role policy, this check is thwarted, but we will be using
|
|
// the role policy anyway.
|
|
return c, config.Errorf("Role Policy (=`%s`) and Claim Name (=`%s`) cannot both be set", p.RolePolicy, p.ClaimName)
|
|
}
|
|
|
|
jwksURL := p.DiscoveryDoc.JwksURI
|
|
if jwksURL == "" {
|
|
return c, config.Errorf("no JWKS URI found in your provider's discovery doc (config_url=%s)", configURL)
|
|
}
|
|
|
|
p.JWKS.URL, err = xnet.ParseHTTPURL(jwksURL)
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
|
|
if p.RolePolicy != "" {
|
|
// RolePolicy is validated by IAM System during its
|
|
// initialization.
|
|
|
|
// Generate role ARN as combination of provider domain and
|
|
// prefix of client ID.
|
|
domain := configURLDomain
|
|
if domain == "" {
|
|
// Attempt to parse the JWKs URI.
|
|
domain = p.JWKS.URL.Hostname()
|
|
if domain == "" {
|
|
return c, config.Errorf("unable to parse a domain from the OpenID config")
|
|
}
|
|
}
|
|
if p.ClientID == "" {
|
|
return c, config.Errorf("client ID must not be empty")
|
|
}
|
|
|
|
// We set the resource ID of the role arn as a hash of client
|
|
// ID, so we can get a short roleARN that stays the same on
|
|
// restart.
|
|
var resourceID string
|
|
{
|
|
h := sha1.New()
|
|
h.Write([]byte(p.ClientID))
|
|
bs := h.Sum(nil)
|
|
resourceID = base64.RawURLEncoding.EncodeToString(bs)
|
|
}
|
|
p.roleArn, err = arn.NewIAMRoleARN(resourceID, serverRegion)
|
|
if err != nil {
|
|
return c, config.Errorf("unable to generate ARN from the OpenID config: %v", err)
|
|
}
|
|
|
|
c.roleArnPolicyMap[p.roleArn] = p.RolePolicy
|
|
} else if p.ClaimName == "" {
|
|
return c, config.Errorf("A role policy or claim name must be specified")
|
|
}
|
|
|
|
if err = p.initializeProvider(getCfgVal, c.transport); err != nil {
|
|
return c, err
|
|
}
|
|
|
|
arnKey := p.roleArn
|
|
if p.RolePolicy == "" {
|
|
arnKey = DummyRoleARN
|
|
// Ensure that at most one JWT policy claim based provider may be
|
|
// defined.
|
|
if _, ok := c.arnProviderCfgsMap[DummyRoleARN]; ok {
|
|
return c, errSingleProvider
|
|
}
|
|
}
|
|
|
|
c.arnProviderCfgsMap[arnKey] = &p
|
|
c.ProviderCfgs[cfgName] = &p
|
|
|
|
if err = c.PopulatePublicKey(arnKey); err != nil {
|
|
return c, err
|
|
}
|
|
}
|
|
|
|
c.Enabled = true
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// ErrProviderConfigNotFound - represents a non-existing provider error.
|
|
var ErrProviderConfigNotFound = errors.New("provider configuration not found")
|
|
|
|
// GetConfigInfo - returns configuration and related info for the given IDP
|
|
// provider.
|
|
func (r *Config) GetConfigInfo(s config.Config, cfgName string) ([]madmin.IDPCfgInfo, error) {
|
|
openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
present := false
|
|
for _, cfg := range openIDConfigs {
|
|
if cfg == cfgName {
|
|
present = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !present {
|
|
return nil, ErrProviderConfigNotFound
|
|
}
|
|
|
|
kvsrcs, err := s.GetResolvedConfigParams(config.IdentityOpenIDSubSys, cfgName, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := make([]madmin.IDPCfgInfo, 0, len(kvsrcs)+1)
|
|
for _, kvsrc := range kvsrcs {
|
|
// skip default values.
|
|
if kvsrc.Src == config.ValueSourceDef {
|
|
if kvsrc.Key != madmin.EnableKey {
|
|
continue
|
|
}
|
|
// set an explicit on/off from live configuration.
|
|
kvsrc.Value = "off"
|
|
if _, ok := r.ProviderCfgs[cfgName]; ok {
|
|
if r.Enabled {
|
|
kvsrc.Value = "on"
|
|
}
|
|
}
|
|
}
|
|
res = append(res, madmin.IDPCfgInfo{
|
|
Key: kvsrc.Key,
|
|
Value: kvsrc.Value,
|
|
IsCfg: true,
|
|
IsEnv: kvsrc.Src == config.ValueSourceEnv,
|
|
})
|
|
}
|
|
|
|
if provCfg, exists := r.ProviderCfgs[cfgName]; exists && provCfg.RolePolicy != "" {
|
|
// Append roleARN
|
|
res = append(res, madmin.IDPCfgInfo{
|
|
Key: "roleARN",
|
|
Value: provCfg.roleArn.String(),
|
|
IsCfg: false,
|
|
})
|
|
}
|
|
|
|
// sort the structs by the key
|
|
sort.Slice(res, func(i, j int) bool {
|
|
return res[i].Key < res[j].Key
|
|
})
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// GetConfigList - list openID configurations
|
|
func (r *Config) GetConfigList(s config.Config) ([]madmin.IDPListItem, error) {
|
|
openIDConfigs, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var res []madmin.IDPListItem
|
|
for _, cfg := range openIDConfigs {
|
|
pcfg, ok := r.ProviderCfgs[cfg]
|
|
if !ok {
|
|
res = append(res, madmin.IDPListItem{
|
|
Type: "openid",
|
|
Name: cfg,
|
|
Enabled: false,
|
|
})
|
|
} else {
|
|
var roleARN string
|
|
if pcfg.RolePolicy != "" {
|
|
roleARN = pcfg.roleArn.String()
|
|
}
|
|
res = append(res, madmin.IDPListItem{
|
|
Type: "openid",
|
|
Name: cfg,
|
|
Enabled: r.Enabled,
|
|
RoleARN: roleARN,
|
|
})
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// Enabled returns if configURL is enabled.
|
|
func Enabled(kvs config.KVS) bool {
|
|
return kvs.Get(ConfigURL) != ""
|
|
}
|
|
|
|
// GetSettings - fetches OIDC settings for site-replication related validation.
|
|
// NOTE that region must be populated by caller as this package does not know.
|
|
func (r *Config) GetSettings() madmin.OpenIDSettings {
|
|
res := madmin.OpenIDSettings{}
|
|
if !r.Enabled {
|
|
return res
|
|
}
|
|
h := sha256.New()
|
|
for arn, provCfg := range r.arnProviderCfgsMap {
|
|
hashedSecret := ""
|
|
{
|
|
h.Reset()
|
|
h.Write([]byte(provCfg.ClientSecret))
|
|
hashedSecret = base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
if arn != DummyRoleARN {
|
|
if res.Roles == nil {
|
|
res.Roles = make(map[string]madmin.OpenIDProviderSettings)
|
|
}
|
|
res.Roles[arn.String()] = madmin.OpenIDProviderSettings{
|
|
ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
|
|
RolePolicy: provCfg.RolePolicy,
|
|
ClientID: provCfg.ClientID,
|
|
HashedClientSecret: hashedSecret,
|
|
}
|
|
} else {
|
|
res.ClaimProvider = madmin.OpenIDProviderSettings{
|
|
ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
|
|
RolePolicy: provCfg.RolePolicy,
|
|
ClientID: provCfg.ClientID,
|
|
HashedClientSecret: hashedSecret,
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// GetIAMPolicyClaimName - returns the policy claim name for the (at most one)
|
|
// provider configured without a role policy.
|
|
func (r *Config) GetIAMPolicyClaimName() string {
|
|
pCfg, ok := r.arnProviderCfgsMap[DummyRoleARN]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return pCfg.ClaimPrefix + pCfg.ClaimName
|
|
}
|
|
|
|
// LookupUser lookup userid for the provider
|
|
func (r Config) LookupUser(roleArn, userid string) (provider.User, error) {
|
|
// Can safely ignore error here as empty or invalid ARNs will not be
|
|
// mapped.
|
|
arnVal, _ := arn.Parse(roleArn)
|
|
pCfg, ok := r.arnProviderCfgsMap[arnVal]
|
|
if ok {
|
|
user, err := pCfg.provider.LookupUser(userid)
|
|
if err != nil && err != provider.ErrAccessTokenExpired {
|
|
return user, err
|
|
}
|
|
if err == provider.ErrAccessTokenExpired {
|
|
if err = pCfg.provider.LoginWithClientID(pCfg.ClientID, pCfg.ClientSecret); err != nil {
|
|
return user, err
|
|
}
|
|
user, err = pCfg.provider.LookupUser(userid)
|
|
}
|
|
return user, err
|
|
}
|
|
// Without any specific logic for a provider, all accounts
|
|
// are always enabled.
|
|
return provider.User{ID: userid, Enabled: true}, nil
|
|
}
|
|
|
|
// ProviderEnabled returns true if any vendor specific provider is enabled.
|
|
func (r Config) ProviderEnabled() bool {
|
|
if !r.Enabled {
|
|
return false
|
|
}
|
|
for _, v := range r.arnProviderCfgsMap {
|
|
if v.provider != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetRoleInfo - returns ARN to policies map if a role policy based openID
|
|
// provider is configured. Otherwise returns nil.
|
|
func (r Config) GetRoleInfo() map[arn.ARN]string {
|
|
for _, p := range r.arnProviderCfgsMap {
|
|
if p.RolePolicy != "" {
|
|
return r.roleArnPolicyMap
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetDefaultExpiration - returns the expiration seconds expected.
|
|
func GetDefaultExpiration(dsecs string) (time.Duration, error) {
|
|
defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
|
|
if dsecs != "" {
|
|
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
|
|
if err != nil {
|
|
return 0, auth.ErrInvalidDuration
|
|
}
|
|
|
|
// The duration, in seconds, of the role session.
|
|
// The value can range from 900 seconds (15 minutes)
|
|
// up to 365 days.
|
|
if expirySecs < 900 || expirySecs > 31536000 {
|
|
return 0, auth.ErrInvalidDuration
|
|
}
|
|
|
|
defaultExpiryDuration = time.Duration(expirySecs) * time.Second
|
|
}
|
|
return defaultExpiryDuration, nil
|
|
}
|