mirror of https://github.com/minio/minio.git
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:
parent
5c81d0d89a
commit
464b9d7c80
|
@ -33,6 +33,7 @@ import (
|
||||||
"github.com/minio/minio/internal/config/etcd"
|
"github.com/minio/minio/internal/config/etcd"
|
||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"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"
|
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||||
"github.com/minio/minio/internal/config/storageclass"
|
"github.com/minio/minio/internal/config/storageclass"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
@ -444,6 +445,8 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||||
off = !xldap.Enabled(kv)
|
off = !xldap.Enabled(kv)
|
||||||
case config.IdentityTLSSubSys:
|
case config.IdentityTLSSubSys:
|
||||||
off = !globalSTSTLSConfig.Enabled
|
off = !globalSTSTLSConfig.Enabled
|
||||||
|
case config.IdentityPluginSubSys:
|
||||||
|
off = !idplugin.Enabled(kv)
|
||||||
}
|
}
|
||||||
if off {
|
if off {
|
||||||
s.WriteString(config.KvComment)
|
s.WriteString(config.KvComment)
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"github.com/minio/minio/internal/config/heal"
|
"github.com/minio/minio/internal/config/heal"
|
||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"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"
|
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||||
"github.com/minio/minio/internal/config/notify"
|
"github.com/minio/minio/internal/config/notify"
|
||||||
"github.com/minio/minio/internal/config/policy/opa"
|
"github.com/minio/minio/internal/config/policy/opa"
|
||||||
|
@ -56,6 +57,7 @@ func initHelp() {
|
||||||
config.IdentityLDAPSubSys: xldap.DefaultKVS,
|
config.IdentityLDAPSubSys: xldap.DefaultKVS,
|
||||||
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
||||||
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
||||||
|
config.IdentityPluginSubSys: idplugin.DefaultKVS,
|
||||||
config.PolicyOPASubSys: opa.DefaultKVS,
|
config.PolicyOPASubSys: opa.DefaultKVS,
|
||||||
config.PolicyPluginSubSys: polplugin.DefaultKVS,
|
config.PolicyPluginSubSys: polplugin.DefaultKVS,
|
||||||
config.SiteSubSys: config.DefaultSiteKVS,
|
config.SiteSubSys: config.DefaultSiteKVS,
|
||||||
|
@ -108,6 +110,10 @@ func initHelp() {
|
||||||
Key: config.IdentityTLSSubSys,
|
Key: config.IdentityTLSSubSys,
|
||||||
Description: "enable X.509 TLS certificate SSO support",
|
Description: "enable X.509 TLS certificate SSO support",
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: config.IdentityPluginSubSys,
|
||||||
|
Description: "enable Identity Plugin via external hook",
|
||||||
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.PolicyPluginSubSys,
|
Key: config.PolicyPluginSubSys,
|
||||||
Description: "enable Access Management Plugin for policy enforcement",
|
Description: "enable Access Management Plugin for policy enforcement",
|
||||||
|
@ -220,6 +226,7 @@ func initHelp() {
|
||||||
config.IdentityOpenIDSubSys: openid.Help,
|
config.IdentityOpenIDSubSys: openid.Help,
|
||||||
config.IdentityLDAPSubSys: xldap.Help,
|
config.IdentityLDAPSubSys: xldap.Help,
|
||||||
config.IdentityTLSSubSys: xtls.Help,
|
config.IdentityTLSSubSys: xtls.Help,
|
||||||
|
config.IdentityPluginSubSys: idplugin.Help,
|
||||||
config.PolicyOPASubSys: opa.Help,
|
config.PolicyOPASubSys: opa.Help,
|
||||||
config.PolicyPluginSubSys: polplugin.Help,
|
config.PolicyPluginSubSys: polplugin.Help,
|
||||||
config.LoggerWebhookSubSys: logger.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 {
|
if _, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default]); err != nil {
|
||||||
return err
|
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:
|
case config.SubnetSubSys:
|
||||||
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
|
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -541,6 +553,19 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
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],
|
authZPluginCfg, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -561,12 +586,6 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||||
|
|
||||||
setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
|
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])
|
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
|
||||||
|
|
|
@ -41,6 +41,7 @@ import (
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"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"
|
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||||
"github.com/minio/minio/internal/config/storageclass"
|
"github.com/minio/minio/internal/config/storageclass"
|
||||||
|
@ -200,10 +201,13 @@ var (
|
||||||
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
||||||
|
|
||||||
globalStorageClass storageclass.Config
|
globalStorageClass storageclass.Config
|
||||||
|
|
||||||
globalLDAPConfig xldap.Config
|
globalLDAPConfig xldap.Config
|
||||||
globalOpenIDConfig openid.Config
|
globalOpenIDConfig openid.Config
|
||||||
globalSTSTLSConfig xtls.Config
|
globalSTSTLSConfig xtls.Config
|
||||||
|
|
||||||
|
globalAuthNPlugin *idplugin.AuthNPlugin
|
||||||
|
|
||||||
// CA root certificates, a nil value means system certs pool will be used
|
// CA root certificates, a nil value means system certs pool will be used
|
||||||
globalRootCAs *x509.CertPool
|
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.
|
// Start watching changes to storage.
|
||||||
go sys.watch(ctx)
|
go sys.watch(ctx)
|
||||||
|
|
||||||
// Load RoleARN
|
// Load RoleARNs
|
||||||
if rolePolicyMap := sys.openIDConfig.GetRoleInfo(); rolePolicyMap != nil {
|
sys.rolesMap = make(map[arn.ARN]string)
|
||||||
// Validate that policies associated with roles are defined.
|
|
||||||
for _, rolePolicies := range rolePolicyMap {
|
// From OpenID
|
||||||
ps := newMappedPolicy(rolePolicies).toSlice()
|
if riMap := globalOpenIDConfig.GetRoleInfo(); riMap != nil {
|
||||||
numPolicies := len(ps)
|
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||||
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
|
}
|
||||||
numValidPolicies := len(strings.Split(validPolicies, ","))
|
|
||||||
if numPolicies != numValidPolicies {
|
// From AuthN plugin if enabled.
|
||||||
logger.LogIf(ctx, fmt.Errorf("Some specified role policies (in %s) were not defined - role policies will not be enabled.", rolePolicies))
|
if globalAuthNPlugin != nil {
|
||||||
return
|
riMap := globalAuthNPlugin.GetRoleInfo()
|
||||||
}
|
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||||
}
|
|
||||||
sys.rolesMap = rolePolicyMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.printIAMRoles()
|
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())
|
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.
|
// Prints IAM role ARNs.
|
||||||
func (sys *IAMSys) printIAMRoles() {
|
func (sys *IAMSys) printIAMRoles() {
|
||||||
if len(sys.rolesMap) == 0 {
|
if len(sys.rolesMap) == 0 {
|
||||||
|
|
|
@ -203,3 +203,16 @@ type AssumeRoleWithCertificateResponse struct {
|
||||||
RequestID string `xml:"RequestId,omitempty"`
|
RequestID string `xml:"RequestId,omitempty"`
|
||||||
} `xml:"ResponseMetadata,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
|
ErrSTSInsecureConnection
|
||||||
ErrSTSInvalidClientCertificate
|
ErrSTSInvalidClientCertificate
|
||||||
ErrSTSNotInitialized
|
ErrSTSNotInitialized
|
||||||
|
ErrSTSUpstreamError
|
||||||
ErrSTSInternalError
|
ErrSTSInternalError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -162,6 +163,11 @@ var stsErrCodes = stsErrorCodeMap{
|
||||||
Description: "STS API not initialized, please try again.",
|
Description: "STS API not initialized, please try again.",
|
||||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
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: {
|
ErrSTSInternalError: {
|
||||||
Code: "InternalError",
|
Code: "InternalError",
|
||||||
Description: "We encountered an internal error generating credentials, please try again.",
|
Description: "We encountered an internal error generating credentials, please try again.",
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -54,11 +55,12 @@ const (
|
||||||
stsLDAPPassword = "LDAPPassword"
|
stsLDAPPassword = "LDAPPassword"
|
||||||
|
|
||||||
// STS API action constants
|
// STS API action constants
|
||||||
clientGrants = "AssumeRoleWithClientGrants"
|
clientGrants = "AssumeRoleWithClientGrants"
|
||||||
webIdentity = "AssumeRoleWithWebIdentity"
|
webIdentity = "AssumeRoleWithWebIdentity"
|
||||||
ldapIdentity = "AssumeRoleWithLDAPIdentity"
|
ldapIdentity = "AssumeRoleWithLDAPIdentity"
|
||||||
clientCertificate = "AssumeRoleWithCertificate"
|
clientCertificate = "AssumeRoleWithCertificate"
|
||||||
assumeRole = "AssumeRole"
|
customTokenIdentity = "AssumeRoleWithCustomToken"
|
||||||
|
assumeRole = "AssumeRole"
|
||||||
|
|
||||||
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
|
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
|
||||||
|
|
||||||
|
@ -128,6 +130,11 @@ func registerSTSRouter(router *mux.Router) {
|
||||||
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCertificate)).
|
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCertificate)).
|
||||||
Queries(stsAction, clientCertificate).
|
Queries(stsAction, clientCertificate).
|
||||||
Queries(stsVersion, stsAPIVersion)
|
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) {
|
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)
|
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
||||||
writeSuccessResponseXML(w, encodeResponse(response))
|
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[ErrSTSInsecureConnection-8]
|
||||||
_ = x[ErrSTSInvalidClientCertificate-9]
|
_ = x[ErrSTSInvalidClientCertificate-9]
|
||||||
_ = x[ErrSTSNotInitialized-10]
|
_ = 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 {
|
func (i STSErrorCode) String() string {
|
||||||
if i < 0 || i >= STSErrorCode(len(_STSErrorCode_index)-1) {
|
if i < 0 || i >= STSErrorCode(len(_STSErrorCode_index)-1) {
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Identity Management Plugin Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
To enable the integration of custom authentication methods, MinIO can be configured with an Identity Management Plugin webhook. When configured, this plugin enables the `AssumeRoleWithCustomToken` STS API extension. A user or application can now present a token to the `AssumeRoleWithCustomToken` API, and MinIO verifies this token by sending it to the Identity Management Plugin webhook. This plugin responds with some information and MinIO is able to generate temporary STS credentials to interact with object storage.
|
||||||
|
|
||||||
|
The authentication flow is similar to that of OpenID, however the token is "opaque" to MinIO - it is simply sent to the plugin for verification. CAVEAT: There is no console UI integration for this method of authentication and it is intended primarily for machine authentication.
|
||||||
|
|
||||||
|
It can be configured via MinIO's standard configuration API (i.e. using `mc admin config set/get`), or equivalently with environment variables. For brevity we show only environment variables here:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ mc admin config set myminio identity_plugin --env
|
||||||
|
KEY:
|
||||||
|
identity_plugin enable Identity Plugin via external hook
|
||||||
|
|
||||||
|
ARGS:
|
||||||
|
MINIO_IDENTITY_PLUGIN_URL* (url) plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/path/to/endpoint"
|
||||||
|
MINIO_IDENTITY_PLUGIN_AUTH_TOKEN (string) authorization token for plugin hook endpoint
|
||||||
|
MINIO_IDENTITY_PLUGIN_ROLE_POLICY* (string) policies to apply for plugin authorized users
|
||||||
|
MINIO_IDENTITY_PLUGIN_ROLE_ID (string) unique ID to generate the ARN
|
||||||
|
MINIO_IDENTITY_PLUGIN_COMMENT (sentence) optionally add a comment to this setting
|
||||||
|
```
|
||||||
|
|
||||||
|
If provided, the auth token parameter is sent as an authorization header.
|
||||||
|
|
||||||
|
`MINIO_IDENTITY_PLUGIN_ROLE_POLICY` is a required parameter and can be list of comma separated policy names.
|
||||||
|
|
||||||
|
On setting up the plugin, the MinIO server prints the Role ARN to its log. The Role ARN is generated by default based on the given plugin URL. To avoid this and use a configurable value set a unique role ID via `MINIO_IDENTITY_PLUGIN_ROLE_ID`.
|
||||||
|
|
||||||
|
## REST API call to plugin
|
||||||
|
|
||||||
|
To verify the custom token presented in the `AssumeRoleWithCustomToken` API, MinIO makes a POST request to the configured identity management plugin endpoint and expects a response with some details as shown below:
|
||||||
|
|
||||||
|
### Request `POST` to plugin endpoint
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
|
||||||
|
| Parameter Name | Value Type | Purpose |
|
||||||
|
|----------------|------------|-------------------------------------------------------------------------|
|
||||||
|
| token | string | Token from the AssumeRoleWithCustomToken call for external verification |
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
If the token is valid and access is approved, the plugin must return a `200` (OK) HTTP status code.
|
||||||
|
|
||||||
|
A `200 OK` Response should have `application/json` content-type and body with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": <string>,
|
||||||
|
"maxValiditySeconds": <integer>,
|
||||||
|
"claims": <key-value-pairs>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter Name | Value Type | Purpose |
|
||||||
|
|--------------------|-----------------------------------------|--------------------------------------------------------|
|
||||||
|
| user | string | Identifier for owner of requested credentials |
|
||||||
|
| maxValiditySeconds | integer (>= 900 seconds and < 365 days) | Maximum allowed expiry duration for the credentials |
|
||||||
|
| claims | key-value pairs | Claims to be associated with the requested credentials |
|
||||||
|
|
||||||
|
The keys "exp", "parent" and "sub" in the `claims` object are reserved and if present are ignored by MinIO.
|
||||||
|
|
||||||
|
If the token is not valid or access is not approved, the plugin must return a `403` (forbidden) HTTP status code. The body must have an `application/json` content-type with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reason": <string>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The reason message is returned to the client.
|
||||||
|
|
||||||
|
## Example Plugin Implementation
|
||||||
|
|
||||||
|
A toy example for the Identity Management Plugin is given [here](./identity-manager-plugin.go).
|
|
@ -0,0 +1,86 @@
|
||||||
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeErrorResponse(w http.ResponseWriter, err error) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"reason": fmt.Sprintf("%v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resp struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
MaxValiditySeconds int `json:"maxValiditySeconds"`
|
||||||
|
Claims map[string]interface{} `json:"claims"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokens map[string]Resp = map[string]Resp{
|
||||||
|
"aaa": {
|
||||||
|
User: "Alice",
|
||||||
|
MaxValiditySeconds: 3600,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"groups": []string{"data-science"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"bbb": {
|
||||||
|
User: "Bart",
|
||||||
|
MaxValiditySeconds: 3600,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"groups": []string{"databases"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.FormValue("token")
|
||||||
|
if token == "" {
|
||||||
|
writeErrorResponse(w, errors.New("token parameter not given"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rsp, ok := tokens[token]
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Allowed for token: %s user: %s\n", token, rsp.User)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(rsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", mainHandler)
|
||||||
|
|
||||||
|
log.Print("Listing on :8081")
|
||||||
|
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
cr "github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// LDAP integrated Minio endpoint
|
||||||
|
stsEndpoint string
|
||||||
|
|
||||||
|
// token to use with AssumeRoleWithCustomToken
|
||||||
|
token string
|
||||||
|
|
||||||
|
// Role ARN to use
|
||||||
|
roleArn string
|
||||||
|
|
||||||
|
// Display credentials flag
|
||||||
|
displayCreds bool
|
||||||
|
|
||||||
|
// Credential expiry duration
|
||||||
|
expiryDuration time.Duration
|
||||||
|
|
||||||
|
// Bucket to list
|
||||||
|
bucketToList string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint")
|
||||||
|
flag.StringVar(&token, "t", "", "Token to use with AssumeRoleWithCustomToken STS API (required)")
|
||||||
|
flag.StringVar(&roleArn, "r", "", "RoleARN to use with the request (required)")
|
||||||
|
flag.BoolVar(&displayCreds, "d", false, "Only show generated credentials")
|
||||||
|
flag.DurationVar(&expiryDuration, "e", 0, "Request a duration of validity for the generated credential")
|
||||||
|
flag.StringVar(&bucketToList, "b", "mybucket", "Bucket to list (defaults to mybucket)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
if token == "" || roleArn == "" {
|
||||||
|
flag.PrintDefaults()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The credentials package in minio-go provides an interface to call the
|
||||||
|
// AssumeRoleWithCustomToken STS API.
|
||||||
|
|
||||||
|
var opts []cr.CustomTokenOpt
|
||||||
|
if expiryDuration != 0 {
|
||||||
|
opts = append(opts, cr.CustomTokenValidityOpt(expiryDuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
li, err := cr.NewCustomTokenCredentials(stsEndpoint, token, roleArn, opts...)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error initializing CustomToken Identity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := li.Get()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error retrieving STS credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if displayCreds {
|
||||||
|
fmt.Println("Only displaying credentials:")
|
||||||
|
fmt.Println("AccessKeyID:", v.AccessKeyID)
|
||||||
|
fmt.Println("SecretAccessKey:", v.SecretAccessKey)
|
||||||
|
fmt.Println("SessionToken:", v.SessionToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use generated credentials to authenticate with MinIO server
|
||||||
|
stsEndpointURL, err := url.Parse(stsEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error parsing sts endpoint: %v", err)
|
||||||
|
}
|
||||||
|
copts := &minio.Options{
|
||||||
|
Creds: li,
|
||||||
|
Secure: stsEndpointURL.Scheme == "https",
|
||||||
|
}
|
||||||
|
minioClient, err := minio.New(stsEndpointURL.Host, copts)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error initializing client: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use minIO Client object normally like the regular client.
|
||||||
|
fmt.Printf("Calling list objects on bucket named `%s` with temp creds:\n===\n", bucketToList)
|
||||||
|
objCh := minioClient.ListObjects(context.Background(), bucketToList, minio.ListObjectsOptions{})
|
||||||
|
for obj := range objCh {
|
||||||
|
if obj.Err != nil {
|
||||||
|
log.Fatalf("Listing error: %v", obj.Err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Key: %s\nSize: %d\nLast Modified: %s\n===\n", obj.Key, obj.Size, obj.LastModified)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
# AssumeRoleWithCustomToken [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
To integrate with custom authentication methods using the [Identity Management Plugin](../iam/identity-management-plugin.md)), MinIO provides an STS API extension called `AssumeRoleWithCustomToken`.
|
||||||
|
|
||||||
|
After configuring the plugin, use the generated Role ARN with `AssumeRoleWithCustomToken` to get temporary credentials to access object storage.
|
||||||
|
|
||||||
|
## API Request
|
||||||
|
|
||||||
|
To make an STS API request with this method, send a POST request to the MinIO endpoint with following query parameters:
|
||||||
|
|
||||||
|
| Parameter | Type | Required | |
|
||||||
|
|-----------------|---------|----------|----------------------------------------------------------------------|
|
||||||
|
| Action | String | Yes | Value must be `AssumeRoleWithCustomToken` |
|
||||||
|
| Version | String | Yes | Value must be `2011-06-15` |
|
||||||
|
| Token | String | Yes | Token to be authenticated by identity plugin |
|
||||||
|
| RoleArn | String | Yes | Must match the Role ARN generated for the identity plugin |
|
||||||
|
| DurationSeconds | Integer | No | Duration of validity of generated credentials. Must be at least 900. |
|
||||||
|
|
||||||
|
The validity duration of the generated STS credentials is the minimum of the `DurationSeconds` parameter (if passed) and the validity duration returned by the Identity Management Plugin.
|
||||||
|
|
||||||
|
## API Response
|
||||||
|
|
||||||
|
XML response for this API is similar to [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_ResponseElements)
|
||||||
|
|
||||||
|
## Example request and response
|
||||||
|
|
||||||
|
Sample request with `curl`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -XPOST 'http://localhost:9001/?Action=AssumeRoleWithCustomToken&Version=2011-06-15&Token=aaa&RoleArn=arn:minio:iam:::role/idmp-vGxBdLkOc8mQPU1-UQbBh-yWWVQ'
|
||||||
|
```
|
||||||
|
|
||||||
|
Prettified Response:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<AssumeRoleWithCustomTokenResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
|
||||||
|
<AssumeRoleWithCustomTokenResult>
|
||||||
|
<Credentials>
|
||||||
|
<AccessKeyId>24Y5H9VHE14H47GEOKCX</AccessKeyId>
|
||||||
|
<SecretAccessKey>H+aBfQ9B1AeWWb++84hvp4tlFBo9aP+hUTdLFIeg</SecretAccessKey>
|
||||||
|
<Expiration>2022-05-25T19:56:34Z</Expiration>
|
||||||
|
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiIyNFk1SDlWSEUxNEg0N0dFT0tDWCIsImV4cCI6MTY1MzUwODU5NCwiZ3JvdXBzIjpbImRhdGEtc2NpZW5jZSJdLCJwYXJlbnQiOiJjdXN0b206QWxpY2UiLCJyb2xlQXJuIjoiYXJuOm1pbmlvOmlhbTo6OnJvbGUvaWRtcC14eHgiLCJzdWIiOiJjdXN0b206QWxpY2UifQ.1tO1LmlUNXiy-wl-ZbkJLWTpaPlhaGqHehsi21lNAmAGCImHHsPb-GA4lRq6GkvHAODN5ZYCf_S-OwpOOdxFwA</SessionToken>
|
||||||
|
</Credentials>
|
||||||
|
<AssumedUser>custom:Alice</AssumedUser>
|
||||||
|
</AssumeRoleWithCustomTokenResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>16F26E081E36DE63</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</AssumeRoleWithCustomTokenResponse>
|
||||||
|
```
|
|
@ -73,6 +73,7 @@ const (
|
||||||
IdentityOpenIDSubSys = "identity_openid"
|
IdentityOpenIDSubSys = "identity_openid"
|
||||||
IdentityLDAPSubSys = "identity_ldap"
|
IdentityLDAPSubSys = "identity_ldap"
|
||||||
IdentityTLSSubSys = "identity_tls"
|
IdentityTLSSubSys = "identity_tls"
|
||||||
|
IdentityPluginSubSys = "identity_plugin"
|
||||||
CacheSubSys = "cache"
|
CacheSubSys = "cache"
|
||||||
SiteSubSys = "site"
|
SiteSubSys = "site"
|
||||||
RegionSubSys = "region"
|
RegionSubSys = "region"
|
||||||
|
@ -146,6 +147,7 @@ var SubSystems = set.CreateStringSet(
|
||||||
IdentityLDAPSubSys,
|
IdentityLDAPSubSys,
|
||||||
IdentityOpenIDSubSys,
|
IdentityOpenIDSubSys,
|
||||||
IdentityTLSSubSys,
|
IdentityTLSSubSys,
|
||||||
|
IdentityPluginSubSys,
|
||||||
ScannerSubSys,
|
ScannerSubSys,
|
||||||
HealSubSys,
|
HealSubSys,
|
||||||
NotifyAMQPSubSys,
|
NotifyAMQPSubSys,
|
||||||
|
@ -188,6 +190,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
|
||||||
PolicyPluginSubSys,
|
PolicyPluginSubSys,
|
||||||
IdentityLDAPSubSys,
|
IdentityLDAPSubSys,
|
||||||
IdentityTLSSubSys,
|
IdentityTLSSubSys,
|
||||||
|
IdentityPluginSubSys,
|
||||||
HealSubSys,
|
HealSubSys,
|
||||||
ScannerSubSys,
|
ScannerSubSys,
|
||||||
}...)
|
}...)
|
||||||
|
|
|
@ -0,0 +1,322 @@
|
||||||
|
// 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/minio/minio/internal/arn"
|
||||||
|
"github.com/minio/minio/internal/config"
|
||||||
|
"github.com/minio/pkg/env"
|
||||||
|
xnet "github.com/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authentication Plugin config and env variables
|
||||||
|
const (
|
||||||
|
URL = "url"
|
||||||
|
AuthToken = "auth_token"
|
||||||
|
RolePolicy = "role_policy"
|
||||||
|
RoleID = "role_id"
|
||||||
|
|
||||||
|
EnvIdentityPluginURL = "MINIO_IDENTITY_PLUGIN_URL"
|
||||||
|
EnvIdentityPluginAuthToken = "MINIO_IDENTITY_PLUGIN_AUTH_TOKEN"
|
||||||
|
EnvIdentityPluginRolePolicy = "MINIO_IDENTITY_PLUGIN_ROLE_POLICY"
|
||||||
|
EnvIdentityPluginRoleID = "MINIO_IDENTITY_PLUGIN_ROLE_ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultKVS - default config for AuthN plugin config
|
||||||
|
DefaultKVS = config.KVS{
|
||||||
|
config.KV{
|
||||||
|
Key: URL,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: AuthToken,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: RolePolicy,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: RoleID,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultHelpPostfix = func(key string) string {
|
||||||
|
return config.DefaultHelpPostfix(DefaultKVS, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help for Identity Plugin
|
||||||
|
Help = config.HelpKVS{
|
||||||
|
config.HelpKV{
|
||||||
|
Key: URL,
|
||||||
|
Description: `plugin hook endpoint (HTTP(S)) e.g. "http://localhost:8181/path/to/endpoint"` + defaultHelpPostfix(URL),
|
||||||
|
Type: "url",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: AuthToken,
|
||||||
|
Description: "authorization token for plugin hook endpoint" + defaultHelpPostfix(AuthToken),
|
||||||
|
Optional: true,
|
||||||
|
Type: "string",
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: RolePolicy,
|
||||||
|
Description: "policies to apply for plugin authorized users" + defaultHelpPostfix(RolePolicy),
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: RoleID,
|
||||||
|
Description: "unique ID to generate the ARN" + defaultHelpPostfix(RoleID),
|
||||||
|
Optional: true,
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: config.Comment,
|
||||||
|
Description: config.DefaultComment,
|
||||||
|
Optional: true,
|
||||||
|
Type: "sentence",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Allows only Base64 URL encoding characters.
|
||||||
|
var validRoleIDRegex = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||||
|
|
||||||
|
// Args for authentication plugin.
|
||||||
|
type Args struct {
|
||||||
|
URL *xnet.URL
|
||||||
|
AuthToken string
|
||||||
|
Transport http.RoundTripper
|
||||||
|
CloseRespFn func(r io.ReadCloser)
|
||||||
|
|
||||||
|
RolePolicy string
|
||||||
|
RoleARN arn.ARN
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate - validate configuration params.
|
||||||
|
func (a *Args) Validate() error {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, a.URL.String(), bytes.NewReader([]byte("")))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if a.AuthToken != "" {
|
||||||
|
req.Header.Set("Authorization", a.AuthToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Transport: a.Transport}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer a.CloseRespFn(resp.Body)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthNPlugin - implements pluggable authentication via webhook.
|
||||||
|
type AuthNPlugin struct {
|
||||||
|
args Args
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled returns if AuthNPlugin is enabled.
|
||||||
|
func Enabled(kvs config.KVS) bool {
|
||||||
|
return kvs.Get(URL) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupConfig lookup AuthNPlugin from config, override with any ENVs.
|
||||||
|
func LookupConfig(kv config.KVS, transport *http.Transport, closeRespFn func(io.ReadCloser), serverRegion string) (Args, error) {
|
||||||
|
args := Args{}
|
||||||
|
|
||||||
|
if err := config.CheckValidKeys(config.IdentityPluginSubSys, kv, DefaultKVS); err != nil {
|
||||||
|
return args, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginURL := env.Get(EnvIdentityPluginURL, kv.Get(URL))
|
||||||
|
if pluginURL == "" {
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
authToken := env.Get(EnvIdentityPluginAuthToken, kv.Get(AuthToken))
|
||||||
|
|
||||||
|
u, err := xnet.ParseHTTPURL(pluginURL)
|
||||||
|
if err != nil {
|
||||||
|
return args, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rolePolicy := env.Get(EnvIdentityPluginRolePolicy, kv.Get(RolePolicy))
|
||||||
|
if rolePolicy == "" {
|
||||||
|
return args, config.Errorf("A role policy must be specified for Identity Management Plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceID := "idmp-"
|
||||||
|
roleID := env.Get(EnvIdentityPluginRoleID, kv.Get(RoleID))
|
||||||
|
if roleID == "" {
|
||||||
|
// We use a hash of the plugin URL so that the ARN remains
|
||||||
|
// constant across restarts.
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write([]byte(pluginURL))
|
||||||
|
bs := h.Sum(nil)
|
||||||
|
resourceID += base64.RawURLEncoding.EncodeToString(bs)
|
||||||
|
} else {
|
||||||
|
// Check that the roleID is restricted to URL safe characters
|
||||||
|
// (base64 URL encoding chars).
|
||||||
|
if !validRoleIDRegex.MatchString(roleID) {
|
||||||
|
return args, config.Errorf("Role ID must match the regexp `^[a-zA-Z0-9_-]+$`")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the user provided ID here.
|
||||||
|
resourceID += roleID
|
||||||
|
}
|
||||||
|
|
||||||
|
roleArn, err := arn.NewIAMRoleARN(resourceID, serverRegion)
|
||||||
|
if err != nil {
|
||||||
|
return args, config.Errorf("unable to generate ARN from the plugin config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = Args{
|
||||||
|
URL: u,
|
||||||
|
AuthToken: authToken,
|
||||||
|
Transport: transport,
|
||||||
|
CloseRespFn: closeRespFn,
|
||||||
|
RolePolicy: rolePolicy,
|
||||||
|
RoleARN: roleArn,
|
||||||
|
}
|
||||||
|
if err = args.Validate(); err != nil {
|
||||||
|
return args, err
|
||||||
|
}
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New - initializes Authorization Management Plugin.
|
||||||
|
func New(args Args) *AuthNPlugin {
|
||||||
|
if args.URL == nil || args.URL.Scheme == "" && args.AuthToken == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &AuthNPlugin{
|
||||||
|
args: args,
|
||||||
|
client: &http.Client{Transport: args.Transport},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthNSuccessResponse - represents the response from the authentication plugin
|
||||||
|
// service.
|
||||||
|
type AuthNSuccessResponse struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
MaxValiditySeconds int `json:"maxValiditySeconds"`
|
||||||
|
Claims map[string]interface{} `json:"claims"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthNErrorResponse - represents an error response from the authN plugin.
|
||||||
|
type AuthNErrorResponse struct {
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthNResponse - represents a result of the authentication operation.
|
||||||
|
type AuthNResponse struct {
|
||||||
|
Success *AuthNSuccessResponse
|
||||||
|
Failure *AuthNErrorResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
minValidityDurationSeconds int = 900
|
||||||
|
maxValidityDurationSeconds int = 365 * 24 * 3600
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authenticate authenticates the token with the external hook endpoint and
|
||||||
|
// returns a parent user, max expiry duration for the authentication and a set
|
||||||
|
// of claims.
|
||||||
|
func (o *AuthNPlugin) Authenticate(roleArn arn.ARN, token string) (AuthNResponse, error) {
|
||||||
|
if o == nil {
|
||||||
|
return AuthNResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if roleArn != o.args.RoleARN {
|
||||||
|
return AuthNResponse{}, fmt.Errorf("Invalid role ARN value: %s", roleArn.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var u url.URL = url.URL(*o.args.URL)
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("token", token)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return AuthNResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.args.AuthToken != "" {
|
||||||
|
req.Header.Set("Authorization", o.args.AuthToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := o.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return AuthNResponse{}, err
|
||||||
|
}
|
||||||
|
defer o.args.CloseRespFn(resp.Body)
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200:
|
||||||
|
var result AuthNSuccessResponse
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return AuthNResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.MaxValiditySeconds < minValidityDurationSeconds || result.MaxValiditySeconds > maxValidityDurationSeconds {
|
||||||
|
return AuthNResponse{}, fmt.Errorf("Plugin returned an invalid validity duration (%d) - should be between %d and %d",
|
||||||
|
result.MaxValiditySeconds, minValidityDurationSeconds, maxValidityDurationSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthNResponse{
|
||||||
|
Success: &result,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
case 403:
|
||||||
|
var result AuthNErrorResponse
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return AuthNResponse{}, err
|
||||||
|
}
|
||||||
|
return AuthNResponse{
|
||||||
|
Failure: &result,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return AuthNResponse{}, fmt.Errorf("Invalid status code %d from auth plugin", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoleInfo - returns ARN to policies map.
|
||||||
|
func (o *AuthNPlugin) GetRoleInfo() map[arn.ARN]string {
|
||||||
|
return map[arn.ARN]string{
|
||||||
|
o.args.RoleARN: o.args.RolePolicy,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue