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:
Aditya Manthramurthy
2022-05-26 17:58:09 -07:00
committed by GitHub
parent 5c81d0d89a
commit 464b9d7c80
14 changed files with 888 additions and 28 deletions

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -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.",

View File

@@ -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))
}

View File

@@ -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) {