mirror of
https://github.com/minio/minio.git
synced 2025-11-10 14:09:48 -05:00
Add support for Identity Management Plugin (#14913)
- Adds an STS API `AssumeRoleWithCustomToken` that can be used to authenticate via the Id. Mgmt. Plugin. - Adds a sample identity manager plugin implementation - Add doc for plugin and STS API - Add an example program using go SDK for AssumeRoleWithCustomToken
This commit is contained in:
committed by
GitHub
parent
5c81d0d89a
commit
464b9d7c80
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/minio/minio/internal/config/etcd"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -444,6 +445,8 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
off = !xldap.Enabled(kv)
|
||||
case config.IdentityTLSSubSys:
|
||||
off = !globalSTSTLSConfig.Enabled
|
||||
case config.IdentityPluginSubSys:
|
||||
off = !idplugin.Enabled(kv)
|
||||
}
|
||||
if off {
|
||||
s.WriteString(config.KvComment)
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/minio/minio/internal/config/heal"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||
"github.com/minio/minio/internal/config/notify"
|
||||
"github.com/minio/minio/internal/config/policy/opa"
|
||||
@@ -56,6 +57,7 @@ func initHelp() {
|
||||
config.IdentityLDAPSubSys: xldap.DefaultKVS,
|
||||
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
||||
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
||||
config.IdentityPluginSubSys: idplugin.DefaultKVS,
|
||||
config.PolicyOPASubSys: opa.DefaultKVS,
|
||||
config.PolicyPluginSubSys: polplugin.DefaultKVS,
|
||||
config.SiteSubSys: config.DefaultSiteKVS,
|
||||
@@ -108,6 +110,10 @@ func initHelp() {
|
||||
Key: config.IdentityTLSSubSys,
|
||||
Description: "enable X.509 TLS certificate SSO support",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.IdentityPluginSubSys,
|
||||
Description: "enable Identity Plugin via external hook",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.PolicyPluginSubSys,
|
||||
Description: "enable Access Management Plugin for policy enforcement",
|
||||
@@ -220,6 +226,7 @@ func initHelp() {
|
||||
config.IdentityOpenIDSubSys: openid.Help,
|
||||
config.IdentityLDAPSubSys: xldap.Help,
|
||||
config.IdentityTLSSubSys: xtls.Help,
|
||||
config.IdentityPluginSubSys: idplugin.Help,
|
||||
config.PolicyOPASubSys: opa.Help,
|
||||
config.PolicyPluginSubSys: polplugin.Help,
|
||||
config.LoggerWebhookSubSys: logger.Help,
|
||||
@@ -342,6 +349,11 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
||||
if _, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default]); err != nil {
|
||||
return err
|
||||
}
|
||||
case config.IdentityPluginSubSys:
|
||||
if _, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
|
||||
return err
|
||||
}
|
||||
case config.SubnetSubSys:
|
||||
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
|
||||
return err
|
||||
@@ -541,6 +553,19 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
||||
}
|
||||
|
||||
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||
globalRootCAs)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
|
||||
}
|
||||
|
||||
authNPluginCfg, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthNPlugin: %w", err))
|
||||
}
|
||||
globalAuthNPlugin = idplugin.New(authNPluginCfg)
|
||||
|
||||
authZPluginCfg, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||
if err != nil {
|
||||
@@ -561,12 +586,6 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
|
||||
setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
|
||||
|
||||
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||
globalRootCAs)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
|
||||
}
|
||||
|
||||
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
@@ -200,10 +201,13 @@ var (
|
||||
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
||||
|
||||
globalStorageClass storageclass.Config
|
||||
|
||||
globalLDAPConfig xldap.Config
|
||||
globalOpenIDConfig openid.Config
|
||||
globalSTSTLSConfig xtls.Config
|
||||
|
||||
globalAuthNPlugin *idplugin.AuthNPlugin
|
||||
|
||||
// CA root certificates, a nil value means system certs pool will be used
|
||||
globalRootCAs *x509.CertPool
|
||||
|
||||
|
||||
52
cmd/iam.go
52
cmd/iam.go
@@ -353,20 +353,18 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
// Start watching changes to storage.
|
||||
go sys.watch(ctx)
|
||||
|
||||
// Load RoleARN
|
||||
if rolePolicyMap := sys.openIDConfig.GetRoleInfo(); rolePolicyMap != nil {
|
||||
// Validate that policies associated with roles are defined.
|
||||
for _, rolePolicies := range rolePolicyMap {
|
||||
ps := newMappedPolicy(rolePolicies).toSlice()
|
||||
numPolicies := len(ps)
|
||||
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
|
||||
numValidPolicies := len(strings.Split(validPolicies, ","))
|
||||
if numPolicies != numValidPolicies {
|
||||
logger.LogIf(ctx, fmt.Errorf("Some specified role policies (in %s) were not defined - role policies will not be enabled.", rolePolicies))
|
||||
return
|
||||
}
|
||||
}
|
||||
sys.rolesMap = rolePolicyMap
|
||||
// Load RoleARNs
|
||||
sys.rolesMap = make(map[arn.ARN]string)
|
||||
|
||||
// From OpenID
|
||||
if riMap := globalOpenIDConfig.GetRoleInfo(); riMap != nil {
|
||||
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||
}
|
||||
|
||||
// From AuthN plugin if enabled.
|
||||
if globalAuthNPlugin != nil {
|
||||
riMap := globalAuthNPlugin.GetRoleInfo()
|
||||
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||
}
|
||||
|
||||
sys.printIAMRoles()
|
||||
@@ -375,6 +373,32 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
logger.Info("Finished loading IAM sub-system (took %.1fs of %.1fs to load data).", now.Sub(iamLoadStart).Seconds(), now.Sub(iamInitStart).Seconds())
|
||||
}
|
||||
|
||||
func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[arn.ARN]string) {
|
||||
// Validate that policies associated with roles are defined. If
|
||||
// authZ plugin is set, role policies are just claims sent to
|
||||
// the plugin and they need not exist.
|
||||
//
|
||||
// If some mapped policies do not exist, we print some error
|
||||
// messages but continue any way - they can be fixed in the
|
||||
// running server by creating the policies after start up.
|
||||
for arn, rolePolicies := range m {
|
||||
specifiedPoliciesSet := newMappedPolicy(rolePolicies).policySet()
|
||||
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
|
||||
knownPoliciesSet := newMappedPolicy(validPolicies).policySet()
|
||||
unknownPoliciesSet := specifiedPoliciesSet.Difference(knownPoliciesSet)
|
||||
if len(unknownPoliciesSet) > 0 {
|
||||
if globalAuthZPlugin == nil {
|
||||
// Print a warning that some policies mapped to a role are not defined.
|
||||
errMsg := fmt.Errorf(
|
||||
"The policies \"%s\" mapped to role ARN %s are not defined - this role may not work as expected.",
|
||||
unknownPoliciesSet.ToSlice(), arn.String())
|
||||
logger.LogIf(ctx, errMsg)
|
||||
}
|
||||
}
|
||||
sys.rolesMap[arn] = rolePolicies
|
||||
}
|
||||
}
|
||||
|
||||
// Prints IAM role ARNs.
|
||||
func (sys *IAMSys) printIAMRoles() {
|
||||
if len(sys.rolesMap) == 0 {
|
||||
|
||||
@@ -203,3 +203,16 @@ type AssumeRoleWithCertificateResponse struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// AssumeRoleWithCustomTokenResponse contains the result of a successful
|
||||
// AssumeRoleWithCustomToken request.
|
||||
type AssumeRoleWithCustomTokenResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithCustomTokenResponse" json:"-"`
|
||||
Result struct {
|
||||
Credentials auth.Credentials `xml:"Credentials,omitempty"`
|
||||
AssumedUser string `xml:"AssumedUser,omitempty"`
|
||||
} `xml:"AssumeRoleWithCustomTokenResult"`
|
||||
Metadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ const (
|
||||
ErrSTSInsecureConnection
|
||||
ErrSTSInvalidClientCertificate
|
||||
ErrSTSNotInitialized
|
||||
ErrSTSUpstreamError
|
||||
ErrSTSInternalError
|
||||
)
|
||||
|
||||
@@ -162,6 +163,11 @@ var stsErrCodes = stsErrorCodeMap{
|
||||
Description: "STS API not initialized, please try again.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrSTSUpstreamError: {
|
||||
Code: "InternalError",
|
||||
Description: "An upstream service required for this operation failed - please try again or contact an administrator.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrSTSInternalError: {
|
||||
Code: "InternalError",
|
||||
Description: "We encountered an internal error generating credentials, please try again.",
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,11 +55,12 @@ const (
|
||||
stsLDAPPassword = "LDAPPassword"
|
||||
|
||||
// STS API action constants
|
||||
clientGrants = "AssumeRoleWithClientGrants"
|
||||
webIdentity = "AssumeRoleWithWebIdentity"
|
||||
ldapIdentity = "AssumeRoleWithLDAPIdentity"
|
||||
clientCertificate = "AssumeRoleWithCertificate"
|
||||
assumeRole = "AssumeRole"
|
||||
clientGrants = "AssumeRoleWithClientGrants"
|
||||
webIdentity = "AssumeRoleWithWebIdentity"
|
||||
ldapIdentity = "AssumeRoleWithLDAPIdentity"
|
||||
clientCertificate = "AssumeRoleWithCertificate"
|
||||
customTokenIdentity = "AssumeRoleWithCustomToken"
|
||||
assumeRole = "AssumeRole"
|
||||
|
||||
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
|
||||
|
||||
@@ -128,6 +130,11 @@ func registerSTSRouter(router *mux.Router) {
|
||||
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCertificate)).
|
||||
Queries(stsAction, clientCertificate).
|
||||
Queries(stsVersion, stsAPIVersion)
|
||||
|
||||
// AssumeRoleWithCustomToken
|
||||
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCustomToken)).
|
||||
Queries(stsAction, customTokenIdentity).
|
||||
Queries(stsVersion, stsAPIVersion)
|
||||
}
|
||||
|
||||
func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, isErrCodeSTS bool, stsErr STSErrorCode) {
|
||||
@@ -815,3 +822,125 @@ func (sts *stsAPIHandlers) AssumeRoleWithCertificate(w http.ResponseWriter, r *h
|
||||
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
}
|
||||
|
||||
// AssumeRoleWithCustomToken implements user authentication with custom tokens.
|
||||
// These tokens are opaque to MinIO and are verified by a configured (external)
|
||||
// Identity Management Plugin.
|
||||
//
|
||||
// API endpoint: https://minio:9000?Action=AssumeRoleWithCustomToken&Token=xxx
|
||||
func (sts *stsAPIHandlers) AssumeRoleWithCustomToken(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AssumeRoleWithCustomToken")
|
||||
|
||||
if globalAuthNPlugin == nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSNotInitialized, errors.New("STS API 'AssumeRoleWithCustomToken' is disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
action := r.Form.Get(stsAction)
|
||||
if action != customTokenIdentity {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action))
|
||||
return
|
||||
}
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, nil)
|
||||
|
||||
token := r.Form.Get(stsToken)
|
||||
if token == "" {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid empty `Token` parameter provided"))
|
||||
return
|
||||
}
|
||||
|
||||
durationParam := r.Form.Get(stsDurationSeconds)
|
||||
var requestedDuration int
|
||||
if durationParam != "" {
|
||||
var err error
|
||||
requestedDuration, err = strconv.Atoi(durationParam)
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid requested duration: %s", durationParam))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
roleArnStr := r.Form.Get(stsRoleArn)
|
||||
roleArn, _, err := globalIAMSys.GetRolePolicy(roleArnStr)
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
||||
fmt.Errorf("Error processing parameter %s: %v", stsRoleArn, err))
|
||||
return
|
||||
}
|
||||
|
||||
res, err := globalAuthNPlugin.Authenticate(roleArn, token)
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
|
||||
return
|
||||
}
|
||||
|
||||
// If authentication failed, return the error message to the user.
|
||||
if res.Failure != nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSUpstreamError, errors.New(res.Failure.Reason))
|
||||
return
|
||||
}
|
||||
|
||||
// It is required that parent user be set.
|
||||
if res.Success.User == "" {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSUpstreamError, errors.New("A valid user was not returned by the authenticator."))
|
||||
return
|
||||
}
|
||||
|
||||
// Expiry is set as minimum of requested value and value allowed by auth
|
||||
// plugin.
|
||||
expiry := res.Success.MaxValiditySeconds
|
||||
if durationParam != "" && requestedDuration < expiry {
|
||||
expiry = requestedDuration
|
||||
}
|
||||
|
||||
parentUser := "custom:" + res.Success.User
|
||||
|
||||
// metadata map
|
||||
m := map[string]interface{}{
|
||||
expClaim: UTCNow().Add(time.Duration(expiry) * time.Second).Unix(),
|
||||
parentClaim: parentUser,
|
||||
subClaim: parentUser,
|
||||
roleArnClaim: roleArn.String(),
|
||||
}
|
||||
// Add all other claims from the plugin **without** replacing any
|
||||
// existing claims.
|
||||
for k, v := range res.Success.Claims {
|
||||
if _, ok := m[k]; !ok {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
tmpCredentials, err := auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
||||
return
|
||||
}
|
||||
|
||||
tmpCredentials.ParentUser = parentUser
|
||||
err = globalIAMSys.SetTempUser(ctx, tmpCredentials.AccessKey, tmpCredentials, "")
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call hook for site replication.
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSTSAcc,
|
||||
STSCredential: &madmin.SRSTSCredential{
|
||||
AccessKey: tmpCredentials.AccessKey,
|
||||
SecretKey: tmpCredentials.SecretKey,
|
||||
SessionToken: tmpCredentials.SessionToken,
|
||||
ParentUser: tmpCredentials.ParentUser,
|
||||
},
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
response := new(AssumeRoleWithCustomTokenResponse)
|
||||
response.Result.Credentials = tmpCredentials
|
||||
response.Result.AssumedUser = parentUser
|
||||
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ func _() {
|
||||
_ = x[ErrSTSInsecureConnection-8]
|
||||
_ = x[ErrSTSInvalidClientCertificate-9]
|
||||
_ = x[ErrSTSNotInitialized-10]
|
||||
_ = x[ErrSTSInternalError-11]
|
||||
_ = x[ErrSTSUpstreamError-11]
|
||||
_ = x[ErrSTSInternalError-12]
|
||||
}
|
||||
|
||||
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSInsecureConnectionSTSInvalidClientCertificateSTSNotInitializedSTSInternalError"
|
||||
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSInsecureConnectionSTSInvalidClientCertificateSTSNotInitializedSTSUpstreamErrorSTSInternalError"
|
||||
|
||||
var _STSErrorCode_index = [...]uint8{0, 7, 22, 41, 65, 91, 118, 145, 171, 192, 219, 236, 252}
|
||||
var _STSErrorCode_index = [...]uint16{0, 7, 22, 41, 65, 91, 118, 145, 171, 192, 219, 236, 252, 268}
|
||||
|
||||
func (i STSErrorCode) String() string {
|
||||
if i < 0 || i >= STSErrorCode(len(_STSErrorCode_index)-1) {
|
||||
|
||||
Reference in New Issue
Block a user