mirror of
https://github.com/minio/minio.git
synced 2025-11-09 21:49:46 -05:00
Introduce STS client grants API and OPA policy integration (#6168)
This PR introduces two new features - AWS STS compatible STS API named AssumeRoleWithClientGrants ``` POST /?Action=AssumeRoleWithClientGrants&Token=<jwt> ``` This API endpoint returns temporary access credentials, access tokens signature types supported by this API - RSA keys - ECDSA keys Fetches the required public key from the JWKS endpoints, provides them as rsa or ecdsa public keys. - External policy engine support, in this case OPA policy engine - Credentials are stored on disks
This commit is contained in:
committed by
kannappanr
parent
16a100b597
commit
54ae364def
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -43,7 +44,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxConfigJSONSize = 256 * 1024 // 256KiB
|
||||
maxEConfigJSONSize = 262272
|
||||
)
|
||||
|
||||
// Type-safe query params.
|
||||
@@ -597,7 +598,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
password := config.GetCredential().SecretKey
|
||||
econfigData, err := madmin.EncryptServerConfigData(password, configData)
|
||||
econfigData, err := madmin.EncryptData(password, configData)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
@@ -681,7 +682,7 @@ func (a adminAPIHandlers) GetConfigKeysHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
password := config.GetCredential().SecretKey
|
||||
econfigData, err := madmin.EncryptServerConfigData(password, []byte(newConfigStr))
|
||||
econfigData, err := madmin.EncryptData(password, []byte(newConfigStr))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
@@ -702,6 +703,195 @@ func toAdminAPIErrCode(err error) APIErrorCode {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveUser - DELETE /minio/admin/v1/remove-user?accessKey=<access_key>
|
||||
func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "RemoveUser")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
if err := globalIAMSys.DeleteUser(accessKey); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveUserPolicy - DELETE /minio/admin/v1/remove-user-policy?accessKey=<access_key>
|
||||
func (a adminAPIHandlers) RemoveUserPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "RemoveUserPolicy")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
if err := globalIAMSys.DeletePolicy(accessKey); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// AddUser - PUT /minio/admin/v1/add-user?accessKey=<access_key>
|
||||
func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AddUser")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
|
||||
// Custom IAM policies not allowed for admin user.
|
||||
if accessKey == globalServerConfig.GetCredential().AccessKey {
|
||||
writeErrorResponse(w, ErrInvalidRequest, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
return
|
||||
}
|
||||
var uinfo madmin.UserInfo
|
||||
if err = json.Unmarshal(configBytes, &uinfo); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalIAMSys.SetUser(accessKey, uinfo); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// AddUserPolicy - PUT /minio/admin/v1/add-user-policy?accessKey=<access_key>
|
||||
func (a adminAPIHandlers) AddUserPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AddUserPolicy")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Custom IAM policies not allowed for admin user.
|
||||
if accessKey == globalServerConfig.GetCredential().AccessKey {
|
||||
writeErrorResponse(w, ErrInvalidRequest, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Error out if Content-Length is missing.
|
||||
if r.ContentLength <= 0 {
|
||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Error out if Content-Length is beyond allowed size.
|
||||
if r.ContentLength > maxBucketPolicySize {
|
||||
writeErrorResponse(w, ErrEntityTooLarge, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
iamPolicy, err := iampolicy.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrMalformedPolicy, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Version in policy must not be empty
|
||||
if iamPolicy.Version == "" {
|
||||
writeErrorResponse(w, ErrMalformedPolicy, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalIAMSys.SetPolicy(accessKey, *iamPolicy); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfigHandler - PUT /minio/admin/v1/config
|
||||
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetConfigHandler")
|
||||
@@ -726,22 +916,14 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
// Read configuration bytes from request body.
|
||||
configBuf := make([]byte, maxConfigJSONSize+1)
|
||||
n, err := io.ReadFull(r.Body, configBuf)
|
||||
if err == nil {
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||
return
|
||||
}
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
||||
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
@@ -757,8 +939,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
var config serverConfig
|
||||
err = json.Unmarshal(configBytes, &config)
|
||||
if err != nil {
|
||||
if err = json.Unmarshal(configBytes, &config); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||
return
|
||||
@@ -860,7 +1041,7 @@ func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
return
|
||||
}
|
||||
elem, dErr := madmin.DecryptServerConfigData(password, bytes.NewBuffer([]byte(encryptedElem)))
|
||||
elem, dErr := madmin.DecryptData(password, bytes.NewBuffer([]byte(encryptedElem)))
|
||||
if dErr != nil {
|
||||
logger.LogIf(ctx, dErr)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
@@ -920,11 +1101,10 @@ func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Re
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// UpdateCredsHandler - POST /minio/admin/v1/config/credential
|
||||
// UpdateAdminCredsHandler - POST /minio/admin/v1/config/credential
|
||||
// ----------
|
||||
// Update credentials in a minio server. In a distributed setup,
|
||||
// update all the servers in the cluster.
|
||||
func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
// Update admin credentials in a minio server
|
||||
func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter,
|
||||
r *http.Request) {
|
||||
|
||||
ctx := newContext(r, w, "UpdateCredentialsHandler")
|
||||
@@ -938,7 +1118,7 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
|
||||
// Avoid setting new credentials when they are already passed
|
||||
// by the environment. Deny if WORM is enabled.
|
||||
if globalIsEnvCreds {
|
||||
if globalIsEnvCreds || globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -950,22 +1130,14 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Read configuration bytes from request body.
|
||||
configBuf := make([]byte, maxConfigJSONSize+1)
|
||||
n, err := io.ReadFull(r.Body, configBuf)
|
||||
if err == nil {
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||
return
|
||||
}
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
||||
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
@@ -993,6 +1165,9 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
// Update local credentials in memory.
|
||||
globalServerConfig.SetCredential(creds)
|
||||
|
||||
// Set active creds.
|
||||
globalActiveCred = creds
|
||||
|
||||
if err = saveServerConfig(ctx, objectAPI, globalServerConfig); err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
|
||||
@@ -38,172 +38,184 @@ import (
|
||||
|
||||
var (
|
||||
configJSON = []byte(`{
|
||||
"version": "30",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "",
|
||||
"worm": "off",
|
||||
"storageclass": {
|
||||
"standard": "",
|
||||
"rrs": ""
|
||||
},
|
||||
"cache": {
|
||||
"drives": [],
|
||||
"expiry": 90,
|
||||
"maxuse": 80,
|
||||
"exclude": []
|
||||
},
|
||||
"kms": {
|
||||
"vault": {
|
||||
"endpoint": "",
|
||||
"auth": {
|
||||
"type": "",
|
||||
"approle": {
|
||||
"id": "",
|
||||
"secret": ""
|
||||
}
|
||||
},
|
||||
"key-id": {
|
||||
"name": "",
|
||||
"version": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"deliveryMode": 0,
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "",
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": "",
|
||||
"tls" : {
|
||||
"enable" : false,
|
||||
"skipVerify" : false,
|
||||
"clientAuth" : 0
|
||||
},
|
||||
"sasl" : {
|
||||
"enable" : false,
|
||||
"username" : "",
|
||||
"password" : ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"broker": "",
|
||||
"topic": "",
|
||||
"qos": 0,
|
||||
"clientId": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"reconnectInterval": 0,
|
||||
"keepAliveInterval": 0
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "",
|
||||
"dsnString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "",
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "",
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"logger": {
|
||||
"console": {
|
||||
"enabled": true
|
||||
},
|
||||
"http": {
|
||||
"target1": {
|
||||
"enabled": false,
|
||||
"endpoint": "https://username:password@example.com/api"
|
||||
}
|
||||
}
|
||||
},
|
||||
"compress": {
|
||||
"enabled": false,
|
||||
"extensions":[".txt",".log",".csv",".json"],
|
||||
"mime-types":["text/csv","text/plain","application/json"]
|
||||
}
|
||||
}`)
|
||||
"version": "31",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"worm": "off",
|
||||
"storageclass": {
|
||||
"standard": "",
|
||||
"rrs": ""
|
||||
},
|
||||
"cache": {
|
||||
"drives": [],
|
||||
"expiry": 90,
|
||||
"maxuse": 80,
|
||||
"exclude": []
|
||||
},
|
||||
"kms": {
|
||||
"vault": {
|
||||
"endpoint": "",
|
||||
"auth": {
|
||||
"type": "",
|
||||
"approle": {
|
||||
"id": "",
|
||||
"secret": ""
|
||||
}
|
||||
},
|
||||
"key-id": {
|
||||
"name": "",
|
||||
"version": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"deliveryMode": 0,
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "namespace",
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": "",
|
||||
"tls": {
|
||||
"enable": false,
|
||||
"skipVerify": false,
|
||||
"clientAuth": 0
|
||||
},
|
||||
"sasl": {
|
||||
"enable": false,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"broker": "",
|
||||
"topic": "",
|
||||
"qos": 0,
|
||||
"clientId": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"reconnectInterval": 0,
|
||||
"keepAliveInterval": 0
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "namespace",
|
||||
"dsnString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "namespace",
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "namespace",
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"logger": {
|
||||
"console": {
|
||||
"enabled": true
|
||||
},
|
||||
"http": {
|
||||
"1": {
|
||||
"enabled": false,
|
||||
"endpoint": "https://username:password@example.com/api"
|
||||
}
|
||||
}
|
||||
},
|
||||
"compress": {
|
||||
"enabled": false,
|
||||
"extensions":[".txt",".log",".csv",".json"],
|
||||
"mime-types":["text/csv","text/plain","application/json"]
|
||||
},
|
||||
"openid": {
|
||||
"jwks": {
|
||||
"url": ""
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"opa": {
|
||||
"url": "",
|
||||
"authToken": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
)
|
||||
|
||||
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
||||
@@ -485,6 +497,8 @@ func getServiceCmdRequest(cmd cmdType, cred auth.Credentials, body []byte) (*htt
|
||||
|
||||
// Set body
|
||||
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
req.ContentLength = int64(len(body))
|
||||
|
||||
// Set sha-sum header
|
||||
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
|
||||
|
||||
@@ -615,7 +629,7 @@ func TestServiceSetCreds(t *testing.T) {
|
||||
t.Fatalf("JSONify err: %v", err)
|
||||
}
|
||||
|
||||
ebody, err := madmin.EncryptServerConfigData(credentials.SecretKey, body)
|
||||
ebody, err := madmin.EncryptData(credentials.SecretKey, body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -718,7 +732,7 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
queryVal.Set("config", "")
|
||||
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
econfigJSON, err := madmin.EncryptServerConfigData(password, configJSON)
|
||||
econfigJSON, err := madmin.EncryptData(password, configJSON)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -738,7 +752,7 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
// Check that a very large config file returns an error.
|
||||
{
|
||||
// Make a large enough config string
|
||||
invalidCfg := []byte(strings.Repeat("A", maxConfigJSONSize+1))
|
||||
invalidCfg := []byte(strings.Repeat("A", maxEConfigJSONSize+1))
|
||||
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
||||
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
||||
if err != nil {
|
||||
@@ -768,7 +782,7 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
adminTestBed.router.ServeHTTP(rec, req)
|
||||
respBody := string(rec.Body.Bytes())
|
||||
if rec.Code != http.StatusBadRequest ||
|
||||
!strings.Contains(respBody, "JSON configuration provided has objects with duplicate keys") {
|
||||
!strings.Contains(respBody, "JSON configuration provided is of incorrect format") {
|
||||
t.Errorf("Got unexpected response code or body %d - %s", rec.Code, respBody)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func registerAdminRouter(router *mux.Router) {
|
||||
/// Config operations
|
||||
|
||||
// Update credentials
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateCredentialsHandler))
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
|
||||
// Get config
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||
// Set config
|
||||
@@ -78,4 +78,13 @@ func registerAdminRouter(router *mux.Router) {
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
||||
// Set config keys/values
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
||||
|
||||
// Add user IAM
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
adminV1Router.Methods(http.MethodPut).Path("/add-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddUserPolicy)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// Remove user IAM
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/remove-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUserPolicy)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@ const (
|
||||
ErrAdminConfigNoQuorum
|
||||
ErrAdminConfigTooLarge
|
||||
ErrAdminConfigBadJSON
|
||||
ErrAdminConfigDuplicateKeys
|
||||
ErrAdminCredentialsMismatch
|
||||
ErrInsecureClientRequest
|
||||
ErrObjectTampered
|
||||
@@ -881,11 +882,16 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
ErrAdminConfigTooLarge: {
|
||||
Code: "XMinioAdminConfigTooLarge",
|
||||
Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes",
|
||||
maxConfigJSONSize),
|
||||
maxEConfigJSONSize),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigBadJSON: {
|
||||
Code: "XMinioAdminConfigBadJSON",
|
||||
Description: "JSON configuration provided is of incorrect format",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigDuplicateKeys: {
|
||||
Code: "XMinioAdminConfigDuplicateKeys",
|
||||
Description: "JSON configuration provided has objects with duplicate keys",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
@@ -27,8 +27,10 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
jwtgo "github.com/dgrijalva/jwt-go"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
@@ -86,6 +88,7 @@ const (
|
||||
authTypeSigned
|
||||
authTypeSignedV2
|
||||
authTypeJWT
|
||||
authTypeSTS
|
||||
)
|
||||
|
||||
// Get request authentication type.
|
||||
@@ -106,6 +109,8 @@ func getRequestAuthType(r *http.Request) authType {
|
||||
return authTypePostPolicy
|
||||
} else if _, ok := r.Header["Authorization"]; !ok {
|
||||
return authTypeAnonymous
|
||||
} else if _, ok := r.URL.Query()["Action"]; ok {
|
||||
return authTypeSTS
|
||||
}
|
||||
return authTypeUnknown
|
||||
}
|
||||
@@ -114,7 +119,21 @@ func getRequestAuthType(r *http.Request) authType {
|
||||
// It does not accept presigned or JWT or anonymous requests.
|
||||
func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
|
||||
s3Err := ErrAccessDenied
|
||||
if _, ok := r.Header["X-Amz-Content-Sha256"]; ok && getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) { // we only support V4 (no presign) with auth. body
|
||||
if _, ok := r.Header["X-Amz-Content-Sha256"]; ok &&
|
||||
getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) {
|
||||
// We only support admin credentials to access admin APIs.
|
||||
|
||||
var owner bool
|
||||
_, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||
if s3Err != ErrNone {
|
||||
return s3Err
|
||||
}
|
||||
|
||||
if !owner {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// we only support V4 (no presign) with auth body
|
||||
s3Err = isReqAuthenticated(r, region)
|
||||
}
|
||||
if s3Err != ErrNone {
|
||||
@@ -125,30 +144,62 @@ func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
|
||||
return s3Err
|
||||
}
|
||||
|
||||
func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) APIErrorCode {
|
||||
isOwner := true
|
||||
accountName := globalServerConfig.GetCredential().AccessKey
|
||||
// Fetch the security token set by the client.
|
||||
func getSessionToken(r *http.Request) (token string) {
|
||||
token = r.Header.Get("X-Amz-Security-Token")
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
return r.URL.Query().Get("X-Amz-Security-Token")
|
||||
}
|
||||
|
||||
// Fetch claims in the security token returned by the client and validate the token.
|
||||
func getClaimsFromToken(r *http.Request) (map[string]interface{}, APIErrorCode) {
|
||||
claims := make(map[string]interface{})
|
||||
token := getSessionToken(r)
|
||||
if token == "" {
|
||||
return nil, ErrNone
|
||||
}
|
||||
p := &jwtgo.Parser{}
|
||||
jtoken, err := p.ParseWithClaims(token, jwtgo.MapClaims(claims), stsTokenCallback)
|
||||
if err != nil {
|
||||
return nil, toAPIErrorCode(errAuthentication)
|
||||
}
|
||||
if !jtoken.Valid {
|
||||
return nil, toAPIErrorCode(errAuthentication)
|
||||
}
|
||||
return claims, ErrNone
|
||||
}
|
||||
|
||||
// Check request auth type verifies the incoming http request
|
||||
// - validates the request signature
|
||||
// - validates the policy action if anonymous tests bucket policies if any,
|
||||
// for authenticated requests validates IAM policies.
|
||||
// returns APIErrorCode if any to be replied to the client.
|
||||
func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) (s3Err APIErrorCode) {
|
||||
var accessKey string
|
||||
var owner bool
|
||||
switch getRequestAuthType(r) {
|
||||
case authTypeUnknown:
|
||||
case authTypeUnknown, authTypeStreamingSigned:
|
||||
return ErrAccessDenied
|
||||
case authTypePresignedV2, authTypeSignedV2:
|
||||
if errorCode := isReqAuthenticatedV2(r); errorCode != ErrNone {
|
||||
return errorCode
|
||||
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
||||
return s3Err
|
||||
}
|
||||
accessKey, owner, s3Err = getReqAccessKeyV2(r)
|
||||
case authTypeSigned, authTypePresigned:
|
||||
region := globalServerConfig.GetRegion()
|
||||
switch action {
|
||||
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
||||
region = ""
|
||||
}
|
||||
|
||||
if errorCode := isReqAuthenticated(r, region); errorCode != ErrNone {
|
||||
return errorCode
|
||||
if s3Err = isReqAuthenticated(r, region); s3Err != ErrNone {
|
||||
return s3Err
|
||||
}
|
||||
default:
|
||||
isOwner = false
|
||||
accountName = ""
|
||||
accessKey, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||
}
|
||||
if s3Err != ErrNone {
|
||||
return s3Err
|
||||
}
|
||||
|
||||
// LocationConstraint is valid only for CreateBucketAction.
|
||||
@@ -174,17 +225,36 @@ func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Ac
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
}
|
||||
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
AccountName: accountName,
|
||||
Action: action,
|
||||
claims, s3Err := getClaimsFromToken(r)
|
||||
if s3Err != ErrNone {
|
||||
return s3Err
|
||||
}
|
||||
|
||||
if accessKey == "" {
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
AccountName: accessKey,
|
||||
Action: action,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, locationConstraint),
|
||||
IsOwner: false,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
return ErrNone
|
||||
}
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: accessKey,
|
||||
Action: iampolicy.Action(action),
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, locationConstraint),
|
||||
IsOwner: isOwner,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ObjectName: objectName,
|
||||
IsOwner: owner,
|
||||
Claims: claims,
|
||||
}) {
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -297,3 +367,55 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeErrorResponse(w, ErrSignatureVersionNotSupported, r.URL)
|
||||
}
|
||||
|
||||
// isPutAllowed - check if PUT operation is allowed on the resource, this
|
||||
// call verifies bucket policies and IAM policies, supports multi user
|
||||
// checks etc.
|
||||
func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request) (s3Err APIErrorCode) {
|
||||
var accessKey string
|
||||
var owner bool
|
||||
switch atype {
|
||||
case authTypeUnknown:
|
||||
return ErrAccessDenied
|
||||
case authTypeSignedV2, authTypePresignedV2:
|
||||
accessKey, owner, s3Err = getReqAccessKeyV2(r)
|
||||
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
||||
region := globalServerConfig.GetRegion()
|
||||
accessKey, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||
}
|
||||
if s3Err != ErrNone {
|
||||
return s3Err
|
||||
}
|
||||
|
||||
claims, s3Err := getClaimsFromToken(r)
|
||||
if s3Err != ErrNone {
|
||||
return s3Err
|
||||
}
|
||||
|
||||
if accessKey == "" {
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
AccountName: accessKey,
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
return ErrNone
|
||||
}
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: accessKey,
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
ObjectName: objectName,
|
||||
IsOwner: owner,
|
||||
Claims: claims,
|
||||
}) {
|
||||
return ErrNone
|
||||
}
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if err = saveNotificationConfig(objectAPI, bucketName, config); err != nil {
|
||||
if err = saveNotificationConfig(ctx, objectAPI, bucketName, config); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,12 +28,11 @@ import (
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
dns2 "github.com/miekg/dns"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
// Check for updates and print a notification message
|
||||
@@ -122,6 +121,7 @@ func handleCommonEnvVars() {
|
||||
if err != nil {
|
||||
logger.Fatal(uiErrInvalidCredentials(err), "Unable to validate credentials inherited from the shell environment")
|
||||
}
|
||||
cred.Expiration = timeSentinel
|
||||
|
||||
// credential Envs are set globally.
|
||||
globalIsEnvCreds = true
|
||||
@@ -131,7 +131,7 @@ func handleCommonEnvVars() {
|
||||
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
||||
browserFlag, err := ParseBoolFlag(browser)
|
||||
if err != nil {
|
||||
logger.Fatal(uiErrInvalidBrowserValue(nil).Msg("Unknown value `%s`", browser), "Invalid MINIO_BROWSER environment variable")
|
||||
logger.Fatal(uiErrInvalidBrowserValue(nil).Msg("Unknown value `%s`", browser), "Invalid MINIO_BROWSER value in environment variable")
|
||||
}
|
||||
|
||||
// browser Envs are set globally, this does not represent
|
||||
@@ -162,7 +162,7 @@ func handleCommonEnvVars() {
|
||||
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
|
||||
if globalDomainName != "" {
|
||||
if _, ok = dns2.IsDomainName(globalDomainName); !ok {
|
||||
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", globalDomainName), "Invalid MINIO_DOMAIN environment variable")
|
||||
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", globalDomainName), "Invalid MINIO_DOMAIN value in environment variable")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ func handleCommonEnvVars() {
|
||||
if worm := os.Getenv("MINIO_WORM"); worm != "" {
|
||||
wormFlag, err := ParseBoolFlag(worm)
|
||||
if err != nil {
|
||||
logger.Fatal(uiErrInvalidWormValue(nil).Msg("Unknown value `%s`", worm), "Unable to validate MINIO_WORM environment variable")
|
||||
logger.Fatal(uiErrInvalidWormValue(nil).Msg("Unknown value `%s`", worm), "Invalid MINIO_WORM value in environment variable")
|
||||
}
|
||||
|
||||
// worm Envs are set globally, this does not represent
|
||||
|
||||
119
cmd/config-common.go
Normal file
119
cmd/config-common.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
var errConfigNotFound = errors.New("config file not found")
|
||||
|
||||
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
// Read entire content by setting size to -1
|
||||
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, "", ObjectOptions{}); err != nil {
|
||||
// Treat object not found as config not found.
|
||||
if isErrObjectNotFound(err) {
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
|
||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return config not found on empty content.
|
||||
if buffer.Len() == 0 {
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = objAPI.PutObject(ctx, minioMetaBucket, configFile, hashReader, nil, ObjectOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func readConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) ([]byte, error) {
|
||||
resp, err := client.Get(ctx, configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Count == 0 {
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
for _, ev := range resp.Kvs {
|
||||
if string(ev.Key) == configFile {
|
||||
return ev.Value, nil
|
||||
}
|
||||
}
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
|
||||
// watchConfig - watches for changes on `configFile` on etcd and loads them.
|
||||
func watchConfig(objAPI ObjectLayer, configFile string, loadCfgFn func(ObjectLayer) error) {
|
||||
if globalEtcdClient != nil {
|
||||
for watchResp := range globalEtcdClient.Watch(context.Background(), configFile) {
|
||||
for _, event := range watchResp.Events {
|
||||
if event.IsModify() || event.IsCreate() {
|
||||
loadCfgFn(objAPI)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) error {
|
||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Count == 0 {
|
||||
return errConfigNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
|
||||
if globalEtcdClient != nil {
|
||||
return checkConfigEtcd(ctx, globalEtcdClient, configFile)
|
||||
}
|
||||
|
||||
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile, ObjectOptions{}); err != nil {
|
||||
// Treat object not found as config not found.
|
||||
if isErrObjectNotFound(err) {
|
||||
return errConfigNotFound
|
||||
}
|
||||
|
||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -25,10 +25,11 @@ import (
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/iam/validator"
|
||||
)
|
||||
|
||||
// Steps to move from version N to version N+1
|
||||
@@ -40,9 +41,9 @@ import (
|
||||
// 6. Make changes in config-current_test.go for any test change
|
||||
|
||||
// Config version
|
||||
const serverConfigVersion = "30"
|
||||
const serverConfigVersion = "31"
|
||||
|
||||
type serverConfig = serverConfigV30
|
||||
type serverConfig = serverConfigV31
|
||||
|
||||
var (
|
||||
// globalServerConfig server config.
|
||||
@@ -173,55 +174,55 @@ func (s *serverConfig) Validate() error {
|
||||
// Worm, Cache and StorageClass values are already validated during json unmarshal
|
||||
for _, v := range s.Notify.AMQP {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("amqp: %s", err.Error())
|
||||
return fmt.Errorf("amqp: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.Elasticsearch {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("elasticsearch: %s", err.Error())
|
||||
return fmt.Errorf("elasticsearch: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.Kafka {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("kafka: %s", err.Error())
|
||||
return fmt.Errorf("kafka: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.MQTT {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("mqtt: %s", err.Error())
|
||||
return fmt.Errorf("mqtt: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.MySQL {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("mysql: %s", err.Error())
|
||||
return fmt.Errorf("mysql: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.NATS {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("nats: %s", err.Error())
|
||||
return fmt.Errorf("nats: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.PostgreSQL {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("postgreSQL: %s", err.Error())
|
||||
return fmt.Errorf("postgreSQL: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.Redis {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("redis: %s", err.Error())
|
||||
return fmt.Errorf("redis: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range s.Notify.Webhook {
|
||||
if err := v.Validate(); err != nil {
|
||||
return fmt.Errorf("webhook: %s", err.Error())
|
||||
return fmt.Errorf("webhook: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,17 +504,31 @@ func (s *serverConfig) loadToCachedConfigs() {
|
||||
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
|
||||
}
|
||||
}
|
||||
|
||||
if !globalIsCompressionEnabled {
|
||||
compressionConf := s.GetCompressionConfig()
|
||||
globalCompressExtensions = compressionConf.Extensions
|
||||
globalCompressMimeTypes = compressionConf.MimeTypes
|
||||
globalIsCompressionEnabled = compressionConf.Enabled
|
||||
}
|
||||
|
||||
if globalIAMValidators == nil {
|
||||
globalIAMValidators = getAuthValidators(s)
|
||||
}
|
||||
|
||||
if globalPolicyOPA == nil {
|
||||
if s.Policy.OPA.URL != nil && s.Policy.OPA.URL.String() != "" {
|
||||
globalPolicyOPA = iampolicy.NewOpa(iampolicy.OpaArgs{
|
||||
URL: s.Policy.OPA.URL,
|
||||
AuthToken: s.Policy.OPA.AuthToken,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newConfig - initialize a new server config, saves env parameters if
|
||||
// newSrvConfig - initialize a new server config, saves env parameters if
|
||||
// found, otherwise use default parameters
|
||||
func newConfig(objAPI ObjectLayer) error {
|
||||
func newSrvConfig(objAPI ObjectLayer) error {
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
@@ -564,6 +579,20 @@ func loadConfig(objAPI ObjectLayer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAuthValidators - returns ValidatorList which contains
|
||||
// enabled providers in server config.
|
||||
// A new authentication provider is added like below
|
||||
// * Add a new provider in pkg/iam/validator package.
|
||||
func getAuthValidators(config *serverConfig) *validator.Validators {
|
||||
validators := validator.NewValidators()
|
||||
|
||||
if config.OpenID.JWKS.URL != nil {
|
||||
validators.Add(validator.NewJWT(config.OpenID.JWKS))
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
// getNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
||||
// A new notification target is added like below
|
||||
// * Add a new target in pkg/event/target package.
|
||||
|
||||
@@ -237,7 +237,7 @@ func TestValidateConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
if err = saveConfig(objLayer, configPath, []byte(testCase.configData)); err != nil {
|
||||
if err = saveConfig(context.Background(), objLayer, configPath, []byte(testCase.configData)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = getValidConfig(objLayer)
|
||||
@@ -260,8 +260,16 @@ func TestConfigDiff(t *testing.T) {
|
||||
{&serverConfig{}, nil, "Given configuration is empty"},
|
||||
// 2
|
||||
{
|
||||
&serverConfig{Credential: auth.Credentials{"u1", "p1"}},
|
||||
&serverConfig{Credential: auth.Credentials{"u1", "p2"}},
|
||||
&serverConfig{Credential: auth.Credentials{
|
||||
AccessKey: "u1",
|
||||
SecretKey: "p1",
|
||||
Expiration: timeSentinel,
|
||||
}},
|
||||
&serverConfig{Credential: auth.Credentials{
|
||||
AccessKey: "u1",
|
||||
SecretKey: "p2",
|
||||
Expiration: timeSentinel,
|
||||
}},
|
||||
"Credential configuration differs",
|
||||
},
|
||||
// 3
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@@ -29,6 +30,8 @@ import (
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/iam/validator"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
@@ -2410,7 +2413,7 @@ func migrateV27ToV28() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrates '.minio.sys/config.json' to v30.
|
||||
// Migrates '.minio.sys/config.json' to v31.
|
||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||
if err := migrateV27ToV28MinioSys(objAPI); err != nil {
|
||||
return err
|
||||
@@ -2418,73 +2421,167 @@ func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||
if err := migrateV28ToV29MinioSys(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateV29ToV30MinioSys(objAPI)
|
||||
if err := migrateV29ToV30MinioSys(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateV30ToV31MinioSys(objAPI)
|
||||
}
|
||||
|
||||
func migrateV29ToV30MinioSys(objAPI ObjectLayer) error {
|
||||
func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (bool, []byte, error) {
|
||||
data, err := readConfig(context.Background(), objAPI, configFile)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
var versionConfig struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
vcfg := &versionConfig
|
||||
if err = json.Unmarshal(data, vcfg); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return vcfg.Version == version, data, nil
|
||||
}
|
||||
|
||||
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
srvConfig, err := readServerConfig(context.Background(), objAPI)
|
||||
ok, data, err := checkConfigVersion(objAPI, configFile, "27")
|
||||
if err == errConfigNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config file. %v", err)
|
||||
}
|
||||
if srvConfig.Version != "29" {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
srvConfig.Version = "30"
|
||||
// Init compression config.For future migration, Compression config needs to be copied over from previous version.
|
||||
srvConfig.Compression.Enabled = false
|
||||
srvConfig.Compression.Extensions = globalCompressExtensions
|
||||
srvConfig.Compression.MimeTypes = globalCompressMimeTypes
|
||||
if err = saveServerConfig(context.Background(), objAPI, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from 29 to 30 . %v", err)
|
||||
cfg := &serverConfigV28{}
|
||||
if err = json.Unmarshal(data, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "29", "30")
|
||||
cfg.Version = "28"
|
||||
cfg.KMS = crypto.KMSConfig{}
|
||||
|
||||
data, err = json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV28ToV29MinioSys(objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
srvConfig, err := readServerConfig(context.Background(), objAPI)
|
||||
|
||||
ok, data, err := checkConfigVersion(objAPI, configFile, "28")
|
||||
if err == errConfigNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config file. %v", err)
|
||||
}
|
||||
if srvConfig.Version != "28" {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
srvConfig.Version = "29"
|
||||
if err = saveServerConfig(context.Background(), objAPI, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from â28â to â29â. %v", err)
|
||||
cfg := &serverConfigV29{}
|
||||
if err = json.Unmarshal(data, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Version = "29"
|
||||
data, err = json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘28’ to ‘29’. %v", err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "28", "29")
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
||||
func migrateV29ToV30MinioSys(objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
srvConfig, err := readServerConfig(context.Background(), objAPI)
|
||||
|
||||
ok, data, err := checkConfigVersion(objAPI, configFile, "29")
|
||||
if err == errConfigNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config file. %v", err)
|
||||
}
|
||||
if srvConfig.Version != "27" {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
srvConfig.Version = "28"
|
||||
srvConfig.KMS = crypto.KMSConfig{}
|
||||
if err = saveServerConfig(context.Background(), objAPI, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from â27â to â28â. %v", err)
|
||||
cfg := &serverConfigV30{}
|
||||
if err = json.Unmarshal(data, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||
cfg.Version = "30"
|
||||
// Init compression config.For future migration, Compression config needs to be copied over from previous version.
|
||||
cfg.Compression.Enabled = false
|
||||
cfg.Compression.Extensions = globalCompressExtensions
|
||||
cfg.Compression.MimeTypes = globalCompressMimeTypes
|
||||
|
||||
data, err = json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘29’ to ‘30’. %v", err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "29", "30")
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV30ToV31MinioSys(objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
ok, data, err := checkConfigVersion(objAPI, configFile, "30")
|
||||
if err == errConfigNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config file. %v", err)
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := &serverConfigV31{}
|
||||
if err = json.Unmarshal(data, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Version = "31"
|
||||
cfg.OpenID.JWKS = validator.JWKSArgs{
|
||||
URL: &xnet.URL{},
|
||||
}
|
||||
cfg.Policy.OPA = iampolicy.OpaArgs{
|
||||
URL: &xnet.URL{},
|
||||
AuthToken: "",
|
||||
}
|
||||
|
||||
data, err = json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘30’ to ‘31’. %v", err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "30", "31")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test if a config migration from v2 to v29 is successfully done
|
||||
func TestServerConfigMigrateV2toV29(t *testing.T) {
|
||||
// Test if a config migration from v2 to v30 is successfully done
|
||||
func TestServerConfigMigrateV2toV30(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -222,6 +222,7 @@ func TestServerConfigMigrateV2toV29(t *testing.T) {
|
||||
if globalServerConfig.Credential.AccessKey != accessKey {
|
||||
t.Fatalf("Access key lost during migration, expected: %v, found:%v", accessKey, globalServerConfig.Credential.AccessKey)
|
||||
}
|
||||
|
||||
if globalServerConfig.Credential.SecretKey != secretKey {
|
||||
t.Fatalf("Secret key lost during migration, expected: %v, found: %v", secretKey, globalServerConfig.Credential.SecretKey)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/iam/validator"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
@@ -755,6 +757,9 @@ type serverConfigV28 struct {
|
||||
Logger loggerConfig `json:"logger"`
|
||||
}
|
||||
|
||||
// serverConfigV29 is just like version '28'.
|
||||
type serverConfigV29 serverConfigV28
|
||||
|
||||
// compressionConfig represents the compression settings.
|
||||
type compressionConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -765,8 +770,6 @@ type compressionConfig struct {
|
||||
// serverConfigV30 is just like version '29', stores additionally
|
||||
// extensions and mimetypes fields for compression.
|
||||
type serverConfigV30 struct {
|
||||
quick.Config `json:"-"` // ignore interfaces
|
||||
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
@@ -792,3 +795,45 @@ type serverConfigV30 struct {
|
||||
// Compression configuration
|
||||
Compression compressionConfig `json:"compress"`
|
||||
}
|
||||
|
||||
// serverConfigV31 is just like version '30', with OPA and OpenID configuration.
|
||||
type serverConfigV31 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Worm BoolFlag `json:"worm"`
|
||||
|
||||
// Storage class configuration
|
||||
StorageClass storageClassConfig `json:"storageclass"`
|
||||
|
||||
// Cache configuration
|
||||
Cache CacheConfig `json:"cache"`
|
||||
|
||||
// KMS configuration
|
||||
KMS crypto.KMSConfig `json:"kms"`
|
||||
|
||||
// Notification queue configuration.
|
||||
Notify notifier `json:"notify"`
|
||||
|
||||
// Logger configuration
|
||||
Logger loggerConfig `json:"logger"`
|
||||
|
||||
// Compression configuration
|
||||
Compression compressionConfig `json:"compress"`
|
||||
|
||||
// OpenID configuration
|
||||
OpenID struct {
|
||||
// JWKS validator config.
|
||||
JWKS validator.JWKSArgs `json:"jwks"`
|
||||
} `json:"openid"`
|
||||
|
||||
// External policy enforcements.
|
||||
Policy struct {
|
||||
// OPA configuration.
|
||||
OPA iampolicy.OpaArgs `json:"opa"`
|
||||
|
||||
// Add new external policy enforcements here.
|
||||
} `json:"policy"`
|
||||
}
|
||||
|
||||
134
cmd/config.go
134
cmd/config.go
@@ -20,9 +20,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
@@ -30,7 +27,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
@@ -63,16 +59,10 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverCon
|
||||
}
|
||||
|
||||
// Create a backup of the current config
|
||||
reader, err := readConfig(ctx, objAPI, configFile)
|
||||
oldData, err := readConfig(ctx, objAPI, configFile)
|
||||
if err == nil {
|
||||
var oldData []byte
|
||||
oldData, err = ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupConfigFile := path.Join(minioConfigPrefix, minioConfigBackupFile)
|
||||
err = saveConfig(objAPI, backupConfigFile, oldData)
|
||||
if err != nil {
|
||||
if err = saveConfig(ctx, objAPI, backupConfigFile, oldData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -82,40 +72,18 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverCon
|
||||
}
|
||||
|
||||
// Save the new config in the std config path
|
||||
return saveConfig(objAPI, configFile, data)
|
||||
}
|
||||
|
||||
func readConfigEtcd(configFile string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Count == 0 {
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
for _, ev := range resp.Kvs {
|
||||
if string(ev.Key) == configFile {
|
||||
return ev.Value, nil
|
||||
}
|
||||
}
|
||||
return nil, errConfigNotFound
|
||||
return saveConfig(ctx, objAPI, configFile, data)
|
||||
}
|
||||
|
||||
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, error) {
|
||||
var configData []byte
|
||||
var err error
|
||||
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
if globalEtcdClient != nil {
|
||||
configData, err = readConfigEtcd(configFile)
|
||||
configData, err = readConfigEtcd(ctx, globalEtcdClient, configFile)
|
||||
} else {
|
||||
var reader io.Reader
|
||||
reader, err = readConfig(ctx, objAPI, configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configData, err = ioutil.ReadAll(reader)
|
||||
configData, err = readConfig(ctx, objAPI, configFile)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -130,82 +98,17 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, e
|
||||
}
|
||||
|
||||
var config = &serverConfig{}
|
||||
if err := json.Unmarshal(configData, config); err != nil {
|
||||
if err = json.Unmarshal(configData, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := quick.CheckData(config); err != nil {
|
||||
if err = quick.CheckData(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func checkServerConfigEtcd(configFile string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Count == 0 {
|
||||
return errConfigNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkServerConfig(ctx context.Context, objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
if globalEtcdClient != nil {
|
||||
return checkServerConfigEtcd(configFile)
|
||||
}
|
||||
|
||||
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile, ObjectOptions{}); err != nil {
|
||||
// Convert ObjectNotFound to errConfigNotFound
|
||||
if isErrObjectNotFound(err) {
|
||||
return errConfigNotFound
|
||||
}
|
||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveConfig(objAPI ObjectLayer, configFile string, data []byte) error {
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = objAPI.PutObject(context.Background(), minioMetaBucket, configFile, hashReader, nil, ObjectOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
var errConfigNotFound = errors.New("config file not found")
|
||||
|
||||
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) (*bytes.Buffer, error) {
|
||||
var buffer bytes.Buffer
|
||||
// Read entire content by setting size to -1
|
||||
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, "", ObjectOptions{}); err != nil {
|
||||
// Convert ObjectNotFound and IncompleteBody errors into errConfigNotFound
|
||||
if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
|
||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return config not found on empty content.
|
||||
if buffer.Len() == 0 {
|
||||
return nil, errConfigNotFound
|
||||
}
|
||||
|
||||
return &buffer, nil
|
||||
}
|
||||
|
||||
// ConfigSys - config system.
|
||||
type ConfigSys struct{}
|
||||
|
||||
@@ -257,8 +160,9 @@ func NewConfigSys() *ConfigSys {
|
||||
func migrateConfigToMinioSys(objAPI ObjectLayer) error {
|
||||
defer os.Rename(getConfigFile(), getConfigFile()+".deprecated")
|
||||
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
// Verify if backend already has the file.
|
||||
if err := checkServerConfig(context.Background(), objAPI); err != errConfigNotFound {
|
||||
if err := checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
||||
return err
|
||||
} // if errConfigNotFound proceed to migrate..
|
||||
|
||||
@@ -287,7 +191,14 @@ func initConfig(objAPI ObjectLayer) error {
|
||||
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||
cancel()
|
||||
if err == nil && resp.Count > 0 {
|
||||
return migrateConfig()
|
||||
if err = migrateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migrates etcd ${HOME}/.minio/config.json to '/config/config.json'
|
||||
if err := migrateConfigToMinioSys(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if isFile(getConfigFile()) {
|
||||
@@ -303,10 +214,15 @@ func initConfig(objAPI ObjectLayer) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := checkServerConfig(context.Background(), objAPI); err != nil {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Watch config for changes and reloads them in-memory.
|
||||
go watchConfig(objAPI, configFile, loadConfig)
|
||||
|
||||
if err := checkConfig(context.Background(), objAPI, configFile); err != nil {
|
||||
if err == errConfigNotFound {
|
||||
// Config file does not exist, we create it fresh and return upon success.
|
||||
if err = newConfig(objAPI); err != nil {
|
||||
if err = newSrvConfig(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1018,7 +1018,7 @@ func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
|
||||
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool) {
|
||||
var err error
|
||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||
if err != nil {
|
||||
if err != nil && err != errFileNotFound {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
}
|
||||
@@ -1274,7 +1274,7 @@ func (fs *FSObjects) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error)
|
||||
|
||||
// SetBucketPolicy sets policy on bucket
|
||||
func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
|
||||
return savePolicyConfig(fs, bucket, policy)
|
||||
return savePolicyConfig(ctx, fs, bucket, policy)
|
||||
}
|
||||
|
||||
// GetBucketPolicy will get policy on bucket
|
||||
|
||||
@@ -157,28 +157,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// Create certs path.
|
||||
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
||||
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
// Override any values from ENVs.
|
||||
srvCfg.loadFromEnvs()
|
||||
|
||||
// Load values to cached global values.
|
||||
srvCfg.loadToCachedConfigs()
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
var cacheConfig = globalServerConfig.GetCacheConfig()
|
||||
if len(cacheConfig.Drives) > 0 {
|
||||
var err error
|
||||
// initialize the new disk cache objects.
|
||||
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
|
||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||
}
|
||||
|
||||
// Check and load SSL certificates.
|
||||
var err error
|
||||
globalPublicCerts, globalRootCAs, globalTLSCerts, globalIsSSL, err = getSSLConfig()
|
||||
@@ -189,12 +167,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
|
||||
initNSLock(false) // Enable local namespace lock.
|
||||
|
||||
// Create new notification system.
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, EndpointList{})
|
||||
|
||||
// Create new policy system.
|
||||
globalPolicySys = NewPolicySys()
|
||||
|
||||
router := mux.NewRouter().SkipClean(true)
|
||||
|
||||
// Add healthcheck router
|
||||
@@ -208,6 +180,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(registerWebRouter(router), "Unable to configure web browser")
|
||||
}
|
||||
|
||||
// Enable STS router if etcd is enabled.
|
||||
if globalEtcdClient != nil {
|
||||
registerSTSRouter(router)
|
||||
}
|
||||
|
||||
// Add API router.
|
||||
registerAPIRouter(router)
|
||||
|
||||
@@ -234,8 +211,52 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||
}
|
||||
|
||||
// Create a new config system.
|
||||
globalConfigSys = NewConfigSys()
|
||||
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
// Override any values from ENVs.
|
||||
srvCfg.loadFromEnvs()
|
||||
|
||||
// Load values to cached global values.
|
||||
srvCfg.loadToCachedConfigs()
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
var cacheConfig = globalServerConfig.GetCacheConfig()
|
||||
if len(cacheConfig.Drives) > 0 {
|
||||
var err error
|
||||
// initialize the new disk cache objects.
|
||||
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
|
||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||
}
|
||||
|
||||
// Load logger subsystem
|
||||
loadLoggers()
|
||||
|
||||
// Re-enable logging
|
||||
logger.Disable = false
|
||||
|
||||
// Create new IAM system.
|
||||
globalIAMSys = NewIAMSys()
|
||||
|
||||
// Initialize IAM sys.
|
||||
go globalIAMSys.Init(newObject)
|
||||
|
||||
// Create new policy system.
|
||||
globalPolicySys = NewPolicySys()
|
||||
|
||||
// Initialize policy system.
|
||||
go globalPolicySys.Init(newObject)
|
||||
|
||||
// Create new notification system.
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||
|
||||
// Once endpoints are finalized, initialize the new object api.
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = newObject
|
||||
@@ -256,8 +277,5 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
printGatewayStartupMessage(getAPIEndpoints(gatewayAddr), gatewayName)
|
||||
}
|
||||
|
||||
// Reenable logging
|
||||
logger.Disable = false
|
||||
|
||||
handleSignals()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import (
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/certs"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/iam/validator"
|
||||
)
|
||||
|
||||
// minio configuration related constants.
|
||||
@@ -81,6 +83,8 @@ const (
|
||||
globalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||
// Refresh interval to update in-memory bucket policy cache.
|
||||
globalRefreshBucketPolicyInterval = 5 * time.Minute
|
||||
// Refresh interval to update in-memory iam config cache.
|
||||
globalRefreshIAMInterval = 5 * time.Minute
|
||||
|
||||
// Limit of location constraint XML for unauthenticted PUT bucket operations.
|
||||
maxLocationConstraintSize = 3 * humanize.MiByte
|
||||
@@ -132,6 +136,7 @@ var (
|
||||
|
||||
globalNotificationSys *NotificationSys
|
||||
globalPolicySys *PolicySys
|
||||
globalIAMSys *IAMSys
|
||||
|
||||
// CA root certificates, a nil value means system certs pool will be used
|
||||
globalRootCAs *x509.CertPool
|
||||
@@ -244,6 +249,12 @@ var (
|
||||
// Some standard content-types which we strictly dis-allow for compression.
|
||||
standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"}
|
||||
|
||||
// Authorization validators list.
|
||||
globalIAMValidators *validator.Validators
|
||||
|
||||
// OPA policy system.
|
||||
globalPolicyOPA *iampolicy.Opa
|
||||
|
||||
// Add new variable global values here.
|
||||
)
|
||||
|
||||
|
||||
352
cmd/iam.go
Normal file
352
cmd/iam.go
Normal file
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
const (
|
||||
// IAM configuration directory.
|
||||
iamConfigPrefix = minioConfigPrefix + "/iam"
|
||||
|
||||
// IAM users directory.
|
||||
iamConfigUsersPrefix = iamConfigPrefix + "/users/"
|
||||
|
||||
// IAM sts directory.
|
||||
iamConfigSTSPrefix = iamConfigPrefix + "/sts/"
|
||||
|
||||
// IAM identity file which captures identity credentials.
|
||||
iamIdentityFile = "identity.json"
|
||||
|
||||
// IAM policy file which provides policies for each users.
|
||||
iamPolicyFile = "policy.json"
|
||||
)
|
||||
|
||||
// IAMSys - config system.
|
||||
type IAMSys struct {
|
||||
sync.RWMutex
|
||||
iamUsersMap map[string]auth.Credentials
|
||||
iamPolicyMap map[string]iampolicy.Policy
|
||||
}
|
||||
|
||||
// Load - load iam.json
|
||||
func (sys *IAMSys) Load(objAPI ObjectLayer) error {
|
||||
return sys.Init(objAPI)
|
||||
}
|
||||
|
||||
// Init - initializes config system from iam.json
|
||||
func (sys *IAMSys) Init(objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
|
||||
if err := sys.refresh(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Refresh IAMSys in background.
|
||||
go func() {
|
||||
ticker := time.NewTicker(globalRefreshIAMInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
logger.LogIf(context.Background(), sys.refresh(objAPI))
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// SetPolicy - sets policy to given user name. If policy is empty,
|
||||
// existing policy is removed.
|
||||
func (sys *IAMSys) SetPolicy(accessKey string, p iampolicy.Policy) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile)
|
||||
data, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.IsEmpty() {
|
||||
delete(sys.iamPolicyMap, accessKey)
|
||||
} else {
|
||||
sys.iamPolicyMap[accessKey] = p
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveTempPolicy - this is used for temporary credentials only.
|
||||
func (sys *IAMSys) SaveTempPolicy(accessKey string, p iampolicy.Policy) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
configFile := pathJoin(iamConfigSTSPrefix, accessKey, iamPolicyFile)
|
||||
data, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.IsEmpty() {
|
||||
delete(sys.iamPolicyMap, accessKey)
|
||||
} else {
|
||||
sys.iamPolicyMap[accessKey] = p
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePolicy - sets policy to given user name. If policy is empty,
|
||||
// existing policy is removed.
|
||||
func (sys *IAMSys) DeletePolicy(accessKey string) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamPolicyFile)
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
err := objectAPI.DeleteObject(context.Background(), minioMetaBucket, configFile)
|
||||
|
||||
delete(sys.iamPolicyMap, accessKey)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteUser - set user credentials.
|
||||
func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile)
|
||||
err := objectAPI.DeleteObject(context.Background(), minioMetaBucket, configFile)
|
||||
|
||||
delete(sys.iamUsersMap, accessKey)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetTempUser - set temporary user credentials, these credentials have an expiry.
|
||||
func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
configFile := pathJoin(iamConfigSTSPrefix, accessKey, iamIdentityFile)
|
||||
data, err := json.Marshal(cred)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys.iamUsersMap[accessKey] = cred
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUser - set user credentials.
|
||||
func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile)
|
||||
data, err := json.Marshal(uinfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if err = saveConfig(context.Background(), objectAPI, configFile, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys.iamUsersMap[accessKey] = auth.Credentials{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: uinfo.SecretKey,
|
||||
Status: string(uinfo.Status),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser - get user credentials
|
||||
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
cred, ok = sys.iamUsersMap[accessKey]
|
||||
return cred, ok && cred.IsValid()
|
||||
}
|
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
// If policy is available for given user, check the policy.
|
||||
if p, found := sys.iamPolicyMap[args.AccountName]; found {
|
||||
// If opa is configured, use OPA in conjunction with IAM policies.
|
||||
if globalPolicyOPA != nil {
|
||||
return p.IsAllowed(args) && globalPolicyOPA.IsAllowed(args)
|
||||
}
|
||||
return p.IsAllowed(args)
|
||||
}
|
||||
|
||||
// If no policies are set, let the policy arrive from OPA if any.
|
||||
if globalPolicyOPA != nil {
|
||||
return globalPolicyOPA.IsAllowed(args)
|
||||
}
|
||||
|
||||
// As policy is not available and OPA is not configured, return the owner value.
|
||||
return args.IsOwner
|
||||
}
|
||||
|
||||
// reloadUsers reads an updates users, policies from object layer into user and policy maps.
|
||||
func reloadUsers(objectAPI ObjectLayer, prefix string, usersMap map[string]auth.Credentials, policyMap map[string]iampolicy.Policy) error {
|
||||
marker := ""
|
||||
for {
|
||||
var lo ListObjectsInfo
|
||||
var err error
|
||||
lo, err = objectAPI.ListObjects(context.Background(), minioMetaBucket, prefix, marker, "/", 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
marker = lo.NextMarker
|
||||
for _, prefix := range lo.Prefixes {
|
||||
idFile := pathJoin(prefix, iamIdentityFile)
|
||||
pFile := pathJoin(prefix, iamPolicyFile)
|
||||
cdata, cerr := readConfig(context.Background(), objectAPI, idFile)
|
||||
pdata, perr := readConfig(context.Background(), objectAPI, pFile)
|
||||
if cerr != nil && cerr != errConfigNotFound {
|
||||
return cerr
|
||||
}
|
||||
if perr != nil && perr != errConfigNotFound {
|
||||
return perr
|
||||
}
|
||||
if cerr == errConfigNotFound && perr == errConfigNotFound {
|
||||
continue
|
||||
}
|
||||
if cerr == nil {
|
||||
var cred auth.Credentials
|
||||
if err = json.Unmarshal(cdata, &cred); err != nil {
|
||||
return err
|
||||
}
|
||||
cred.AccessKey = path.Base(prefix)
|
||||
if cred.IsExpired() {
|
||||
// Delete expired identity.
|
||||
objectAPI.DeleteObject(context.Background(), minioMetaBucket, idFile)
|
||||
// Delete expired identity policy.
|
||||
objectAPI.DeleteObject(context.Background(), minioMetaBucket, pFile)
|
||||
continue
|
||||
}
|
||||
usersMap[cred.AccessKey] = cred
|
||||
}
|
||||
if perr == nil {
|
||||
var p iampolicy.Policy
|
||||
if err = json.Unmarshal(pdata, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
policyMap[path.Base(prefix)] = p
|
||||
}
|
||||
}
|
||||
if !lo.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh IAMSys.
|
||||
func (sys *IAMSys) refresh(objAPI ObjectLayer) error {
|
||||
iamUsersMap := make(map[string]auth.Credentials)
|
||||
iamPolicyMap := make(map[string]iampolicy.Policy)
|
||||
|
||||
if err := reloadUsers(objAPI, iamConfigUsersPrefix, iamUsersMap, iamPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := reloadUsers(objAPI, iamConfigSTSPrefix, iamUsersMap, iamPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
sys.iamUsersMap = iamUsersMap
|
||||
sys.iamPolicyMap = iamPolicyMap
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewIAMSys - creates new config system object.
|
||||
func NewIAMSys() *IAMSys {
|
||||
return &IAMSys{
|
||||
iamUsersMap: make(map[string]auth.Credentials),
|
||||
iamPolicyMap: make(map[string]iampolicy.Policy),
|
||||
}
|
||||
}
|
||||
122
cmd/jwt.go
122
cmd/jwt.go
@@ -84,57 +84,127 @@ func authenticateURL(accessKey, secretKey string) (string, error) {
|
||||
return authenticateJWT(accessKey, secretKey, defaultURLJWTExpiry)
|
||||
}
|
||||
|
||||
func keyFuncCallback(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||
func stsTokenCallback(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||
if _, ok := jwtToken.Method.(*jwtgo.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||
if err := jwtToken.Claims.Valid(); err != nil {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
if claims, ok := jwtToken.Claims.(jwtgo.MapClaims); ok {
|
||||
accessKey, ok := claims["accessKey"].(string)
|
||||
if !ok {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
if accessKey == globalServerConfig.GetCredential().AccessKey {
|
||||
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||
}
|
||||
if globalIAMSys == nil {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
_, ok = globalIAMSys.GetUser(accessKey)
|
||||
if !ok {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||
}
|
||||
return nil, errAuthentication
|
||||
}
|
||||
|
||||
func isAuthTokenValid(tokenString string) bool {
|
||||
if tokenString == "" {
|
||||
return false
|
||||
// Callback function used for parsing
|
||||
func webTokenCallback(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||
if _, ok := jwtToken.Method.(*jwtgo.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"])
|
||||
}
|
||||
var claims jwtgo.StandardClaims
|
||||
jwtToken, err := jwtgo.ParseWithClaims(tokenString, &claims, keyFuncCallback)
|
||||
|
||||
if err := jwtToken.Claims.Valid(); err != nil {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
|
||||
if claims, ok := jwtToken.Claims.(*jwtgo.StandardClaims); ok {
|
||||
if claims.Subject == globalServerConfig.GetCredential().AccessKey {
|
||||
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||
}
|
||||
if globalIAMSys == nil {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
cred, ok := globalIAMSys.GetUser(claims.Subject)
|
||||
if !ok {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
return []byte(cred.SecretKey), nil
|
||||
}
|
||||
|
||||
return nil, errAuthentication
|
||||
}
|
||||
|
||||
func parseJWTWithClaims(tokenString string, claims jwtgo.Claims) (*jwtgo.Token, error) {
|
||||
p := &jwtgo.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
jwtToken, err := p.ParseWithClaims(tokenString, claims, webTokenCallback)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return false
|
||||
switch e := err.(type) {
|
||||
case *jwtgo.ValidationError:
|
||||
if e.Inner == nil {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
return nil, e.Inner
|
||||
}
|
||||
return nil, errAuthentication
|
||||
}
|
||||
if err = claims.Valid(); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return false
|
||||
return jwtToken, nil
|
||||
}
|
||||
|
||||
func isAuthTokenValid(token string) bool {
|
||||
_, _, err := webTokenAuthenticate(token)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func webTokenAuthenticate(token string) (jwtgo.StandardClaims, bool, error) {
|
||||
var claims = jwtgo.StandardClaims{}
|
||||
if token == "" {
|
||||
return claims, false, errNoAuthToken
|
||||
}
|
||||
return jwtToken.Valid && claims.Subject == globalServerConfig.GetCredential().AccessKey
|
||||
|
||||
jwtToken, err := parseJWTWithClaims(token, &claims)
|
||||
if err != nil {
|
||||
return claims, false, err
|
||||
}
|
||||
if !jwtToken.Valid {
|
||||
return claims, false, errAuthentication
|
||||
}
|
||||
owner := claims.Subject == globalServerConfig.GetCredential().AccessKey
|
||||
return claims, owner, nil
|
||||
}
|
||||
|
||||
func isHTTPRequestValid(req *http.Request) bool {
|
||||
return webRequestAuthenticate(req) == nil
|
||||
_, _, err := webRequestAuthenticate(req)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Check if the request is authenticated.
|
||||
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
|
||||
// Returns errAuthentication for all other errors.
|
||||
func webRequestAuthenticate(req *http.Request) error {
|
||||
var claims jwtgo.StandardClaims
|
||||
jwtToken, err := jwtreq.ParseFromRequestWithClaims(req, jwtreq.AuthorizationHeaderExtractor, &claims, keyFuncCallback)
|
||||
func webRequestAuthenticate(req *http.Request) (jwtgo.StandardClaims, bool, error) {
|
||||
var claims = jwtgo.StandardClaims{}
|
||||
tokStr, err := jwtreq.AuthorizationHeaderExtractor.ExtractToken(req)
|
||||
if err != nil {
|
||||
if err == jwtreq.ErrNoTokenInRequest {
|
||||
return errNoAuthToken
|
||||
return claims, false, errNoAuthToken
|
||||
}
|
||||
return errAuthentication
|
||||
return claims, false, err
|
||||
}
|
||||
if err = claims.Valid(); err != nil {
|
||||
return errAuthentication
|
||||
}
|
||||
if claims.Subject != globalServerConfig.GetCredential().AccessKey {
|
||||
return errInvalidAccessKeyID
|
||||
jwtToken, err := parseJWTWithClaims(tokStr, &claims)
|
||||
if err != nil {
|
||||
return claims, false, err
|
||||
}
|
||||
if !jwtToken.Valid {
|
||||
return errAuthentication
|
||||
return claims, false, errAuthentication
|
||||
}
|
||||
return nil
|
||||
owner := claims.Subject == globalServerConfig.GetCredential().AccessKey
|
||||
return claims, owner, nil
|
||||
}
|
||||
|
||||
func newAuthToken() string {
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestWebRequestAuthenticate(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
gotErr := webRequestAuthenticate(testCase.req)
|
||||
_, _, gotErr := webRequestAuthenticate(testCase.req)
|
||||
if testCase.expectedErr != gotErr {
|
||||
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
@@ -246,15 +247,14 @@ func (sys *NotificationSys) initListeners(ctx context.Context, objAPI ObjectLaye
|
||||
}
|
||||
defer objLock.RUnlock()
|
||||
|
||||
reader, e := readConfig(ctx, objAPI, configFile)
|
||||
configData, e := readConfig(ctx, objAPI, configFile)
|
||||
if e != nil && !IsErrIgnored(e, errDiskNotFound, errConfigNotFound) {
|
||||
return e
|
||||
}
|
||||
|
||||
listenerList := []ListenBucketNotificationArgs{}
|
||||
if reader != nil {
|
||||
err := json.NewDecoder(reader).Decode(&listenerList)
|
||||
if err != nil {
|
||||
if configData != nil {
|
||||
if err := json.Unmarshal(configData, &listenerList); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
@@ -565,7 +565,7 @@ func sendEvent(args eventArgs) {
|
||||
func readNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName string) (*event.Config, error) {
|
||||
// Construct path to notification.xml for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
||||
reader, err := readConfig(ctx, objAPI, configFile)
|
||||
configData, err := readConfig(ctx, objAPI, configFile)
|
||||
if err != nil {
|
||||
if err == errConfigNotFound {
|
||||
err = errNoSuchNotifications
|
||||
@@ -574,19 +574,19 @@ func readNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := event.ParseConfig(reader, globalServerConfig.GetRegion(), globalNotificationSys.targetList)
|
||||
config, err := event.ParseConfig(bytes.NewReader(configData), globalServerConfig.GetRegion(), globalNotificationSys.targetList)
|
||||
logger.LogIf(ctx, err)
|
||||
return config, err
|
||||
}
|
||||
|
||||
func saveNotificationConfig(objAPI ObjectLayer, bucketName string, config *event.Config) error {
|
||||
func saveNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName string, config *event.Config) error {
|
||||
data, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
||||
return saveConfig(objAPI, configFile, data)
|
||||
return saveConfig(ctx, objAPI, configFile, data)
|
||||
}
|
||||
|
||||
// SaveListener - saves HTTP client currently listening for events to listener.json.
|
||||
@@ -611,14 +611,14 @@ func SaveListener(objAPI ObjectLayer, bucketName string, eventNames []event.Name
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
|
||||
reader, err := readConfig(ctx, objAPI, configFile)
|
||||
configData, err := readConfig(ctx, objAPI, configFile)
|
||||
if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
listenerList := []ListenBucketNotificationArgs{}
|
||||
if reader != nil {
|
||||
if err = json.NewDecoder(reader).Decode(&listenerList); err != nil {
|
||||
if configData != nil {
|
||||
if err = json.Unmarshal(configData, &listenerList); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
@@ -637,7 +637,7 @@ func SaveListener(objAPI ObjectLayer, bucketName string, eventNames []event.Name
|
||||
return err
|
||||
}
|
||||
|
||||
return saveConfig(objAPI, configFile, data)
|
||||
return saveConfig(ctx, objAPI, configFile, data)
|
||||
}
|
||||
|
||||
// RemoveListener - removes HTTP client currently listening for events from listener.json.
|
||||
@@ -662,14 +662,14 @@ func RemoveListener(objAPI ObjectLayer, bucketName string, targetID event.Target
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
|
||||
reader, err := readConfig(ctx, objAPI, configFile)
|
||||
configData, err := readConfig(ctx, objAPI, configFile)
|
||||
if err != nil && !IsErrIgnored(err, errDiskNotFound, errConfigNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
listenerList := []ListenBucketNotificationArgs{}
|
||||
if reader != nil {
|
||||
if err = json.NewDecoder(reader).Decode(&listenerList); err != nil {
|
||||
if configData != nil {
|
||||
if err = json.Unmarshal(configData, &listenerList); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
@@ -696,5 +696,5 @@ func RemoveListener(objAPI ObjectLayer, bucketName string, targetID event.Target
|
||||
return err
|
||||
}
|
||||
|
||||
return saveConfig(objAPI, configFile, data)
|
||||
return saveConfig(ctx, objAPI, configFile, data)
|
||||
}
|
||||
|
||||
@@ -378,12 +378,6 @@ func (e BackendDown) Error() string {
|
||||
return "Backend down"
|
||||
}
|
||||
|
||||
// isErrIncompleteBody - Check if error type is IncompleteBody.
|
||||
func isErrIncompleteBody(err error) bool {
|
||||
_, ok := err.(IncompleteBody)
|
||||
return ok
|
||||
}
|
||||
|
||||
// isErrObjectNotFound - Check if error type is ObjectNotFound.
|
||||
func isErrObjectNotFound(err error) bool {
|
||||
_, ok := err.(ObjectNotFound)
|
||||
|
||||
@@ -1057,22 +1057,14 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
putObject = objectAPI.PutObject
|
||||
)
|
||||
reader = r.Body
|
||||
switch rAuthType {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
||||
|
||||
// Check if put is allowed
|
||||
if s3Err = isPutAllowed(rAuthType, bucket, object, r); s3Err != ErrNone {
|
||||
writeErrorResponse(w, s3Err, r.URL)
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch rAuthType {
|
||||
case authTypeStreamingSigned:
|
||||
// Initialize stream signature verifier.
|
||||
reader, s3Err = newSignV4ChunkedReader(r)
|
||||
@@ -1119,7 +1111,6 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
_, cerr := io.CopyN(snappyWriter, actualReader, actualSize)
|
||||
snappyWriter.Close()
|
||||
pipeWriter.CloseWithError(cerr)
|
||||
|
||||
}()
|
||||
|
||||
// Set compression metrics.
|
||||
@@ -1611,41 +1602,30 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
md5hex = hex.EncodeToString(md5Bytes)
|
||||
sha256hex = ""
|
||||
reader io.Reader
|
||||
s3Error APIErrorCode
|
||||
)
|
||||
reader = r.Body
|
||||
|
||||
switch rAuthType {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
||||
if s3Error = isPutAllowed(rAuthType, bucket, object, r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch rAuthType {
|
||||
case authTypeStreamingSigned:
|
||||
// Initialize stream signature verifier.
|
||||
var s3Error APIErrorCode
|
||||
reader, s3Error = newSignV4ChunkedReader(r)
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
case authTypeSignedV2, authTypePresignedV2:
|
||||
s3Error := isReqAuthenticatedV2(r)
|
||||
if s3Error != ErrNone {
|
||||
if s3Error = isReqAuthenticatedV2(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := reqSignatureV4Verify(r, globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error = reqSignatureV4Verify(r, globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -116,7 +117,7 @@ func (sys *PolicySys) refresh(objAPI ObjectLayer) error {
|
||||
logger.Info("Found in-consistent bucket policies, Migrating them for Bucket: (%s)", bucket.Name)
|
||||
config.Version = policy.DefaultVersion
|
||||
|
||||
if err = savePolicyConfig(objAPI, bucket.Name, config); err != nil {
|
||||
if err = savePolicyConfig(context.Background(), objAPI, bucket.Name, config); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return err
|
||||
}
|
||||
@@ -214,7 +215,7 @@ func getPolicyConfig(objAPI ObjectLayer, bucketName string) (*policy.Policy, err
|
||||
// Construct path to policy.json for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketPolicyConfig)
|
||||
|
||||
reader, err := readConfig(context.Background(), objAPI, configFile)
|
||||
configData, err := readConfig(context.Background(), objAPI, configFile)
|
||||
if err != nil {
|
||||
if err == errConfigNotFound {
|
||||
err = BucketPolicyNotFound{Bucket: bucketName}
|
||||
@@ -223,10 +224,10 @@ func getPolicyConfig(objAPI ObjectLayer, bucketName string) (*policy.Policy, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return policy.ParseConfig(reader, bucketName)
|
||||
return policy.ParseConfig(bytes.NewReader(configData), bucketName)
|
||||
}
|
||||
|
||||
func savePolicyConfig(objAPI ObjectLayer, bucketName string, bucketPolicy *policy.Policy) error {
|
||||
func savePolicyConfig(ctx context.Context, objAPI ObjectLayer, bucketName string, bucketPolicy *policy.Policy) error {
|
||||
data, err := json.Marshal(bucketPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,7 +236,7 @@ func savePolicyConfig(objAPI ObjectLayer, bucketName string, bucketPolicy *polic
|
||||
// Construct path to policy.json for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketPolicyConfig)
|
||||
|
||||
return saveConfig(objAPI, configFile, data)
|
||||
return saveConfig(ctx, objAPI, configFile, data)
|
||||
}
|
||||
|
||||
func removePolicyConfig(ctx context.Context, objAPI ObjectLayer, bucketName string) error {
|
||||
|
||||
@@ -99,6 +99,9 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
|
||||
registerDistXLRouters(router, endpoints)
|
||||
}
|
||||
|
||||
// Add STS router only enabled if etcd is configured.
|
||||
registerSTSRouter(router)
|
||||
|
||||
// Add Admin RPC router
|
||||
registerAdminRPCRouter(router)
|
||||
|
||||
|
||||
@@ -358,11 +358,17 @@ func serverMain(ctx *cli.Context) {
|
||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||
}
|
||||
|
||||
// Create new IAM system.
|
||||
globalIAMSys = NewIAMSys()
|
||||
if err = globalIAMSys.Init(newObject); err != nil {
|
||||
logger.Fatal(err, "Unable to initialize IAM system")
|
||||
}
|
||||
|
||||
// Create new policy system.
|
||||
globalPolicySys = NewPolicySys()
|
||||
|
||||
// Initialize policy system.
|
||||
if err := globalPolicySys.Init(newObject); err != nil {
|
||||
if err = globalPolicySys.Init(newObject); err != nil {
|
||||
logger.Fatal(err, "Unable to initialize policy system")
|
||||
}
|
||||
|
||||
@@ -370,7 +376,7 @@ func serverMain(ctx *cli.Context) {
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||
|
||||
// Initialize notification system.
|
||||
if err := globalNotificationSys.Init(newObject); err != nil {
|
||||
if err = globalNotificationSys.Init(newObject); err != nil {
|
||||
logger.Fatal(err, "Unable to initialize notification system")
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Signature and API related constants.
|
||||
@@ -68,7 +70,14 @@ func doesPolicySignatureV2Match(formValues http.Header) APIErrorCode {
|
||||
cred := globalServerConfig.GetCredential()
|
||||
accessKey := formValues.Get("AWSAccessKeyId")
|
||||
if accessKey != cred.AccessKey {
|
||||
return ErrInvalidAccessKeyID
|
||||
if globalIAMSys == nil {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
var ok bool
|
||||
cred, ok = globalIAMSys.GetUser(accessKey)
|
||||
if !ok {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
}
|
||||
policy := formValues.Get("Policy")
|
||||
signature := formValues.Get("Signature")
|
||||
@@ -146,7 +155,14 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
|
||||
|
||||
// Validate if access key id same.
|
||||
if accessKey != cred.AccessKey {
|
||||
return ErrInvalidAccessKeyID
|
||||
if globalIAMSys == nil {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
var ok bool
|
||||
cred, ok = globalIAMSys.GetUser(accessKey)
|
||||
if !ok {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the request has not expired.
|
||||
@@ -165,7 +181,7 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
|
||||
expectedSignature := preSignatureV2(r.Method, encodedResource, strings.Join(filteredQueries, "&"), r.Header, expires)
|
||||
expectedSignature := preSignatureV2(cred, r.Method, encodedResource, strings.Join(filteredQueries, "&"), r.Header, expires)
|
||||
if !compareSignatureV2(gotSignature, expectedSignature) {
|
||||
return ErrSignatureDoesNotMatch
|
||||
}
|
||||
@@ -173,6 +189,29 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
func getReqAccessKeyV2(r *http.Request) (string, bool, APIErrorCode) {
|
||||
if accessKey := r.URL.Query().Get("AWSAccessKeyId"); accessKey != "" {
|
||||
owner, s3Err := checkKeyValid(accessKey)
|
||||
return accessKey, owner, s3Err
|
||||
}
|
||||
|
||||
// below is V2 Signed Auth header format, splitting on `space` (after the `AWS` string).
|
||||
// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature
|
||||
authFields := strings.Split(r.Header.Get("Authorization"), " ")
|
||||
if len(authFields) != 2 {
|
||||
return "", false, ErrMissingFields
|
||||
}
|
||||
|
||||
// Then will be splitting on ":", this will seprate `AWSAccessKeyId` and `Signature` string.
|
||||
keySignFields := strings.Split(strings.TrimSpace(authFields[1]), ":")
|
||||
if len(keySignFields) != 2 {
|
||||
return "", false, ErrMissingFields
|
||||
}
|
||||
|
||||
owner, s3Err := checkKeyValid(keySignFields[0])
|
||||
return keySignFields[0], owner, s3Err
|
||||
}
|
||||
|
||||
// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;
|
||||
// Signature = Base64( HMAC-SHA1( YourSecretKey, UTF-8-Encoding-Of( StringToSign ) ) );
|
||||
//
|
||||
@@ -193,41 +232,43 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
|
||||
// - http://docs.aws.amazon.com/AmazonS3/latest/dev/auth-request-sig-v2.html
|
||||
// returns true if matches, false otherwise. if error is not nil then it is always false
|
||||
|
||||
func validateV2AuthHeader(v2Auth string) APIErrorCode {
|
||||
func validateV2AuthHeader(r *http.Request) (auth.Credentials, APIErrorCode) {
|
||||
var cred auth.Credentials
|
||||
v2Auth := r.Header.Get("Authorization")
|
||||
if v2Auth == "" {
|
||||
return ErrAuthHeaderEmpty
|
||||
return cred, ErrAuthHeaderEmpty
|
||||
}
|
||||
|
||||
// Verify if the header algorithm is supported or not.
|
||||
if !strings.HasPrefix(v2Auth, signV2Algorithm) {
|
||||
return ErrSignatureVersionNotSupported
|
||||
return cred, ErrSignatureVersionNotSupported
|
||||
}
|
||||
|
||||
// below is V2 Signed Auth header format, splitting on `space` (after the `AWS` string).
|
||||
// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature
|
||||
authFields := strings.Split(v2Auth, " ")
|
||||
if len(authFields) != 2 {
|
||||
return ErrMissingFields
|
||||
}
|
||||
|
||||
// Then will be splitting on ":", this will seprate `AWSAccessKeyId` and `Signature` string.
|
||||
keySignFields := strings.Split(strings.TrimSpace(authFields[1]), ":")
|
||||
if len(keySignFields) != 2 {
|
||||
return ErrMissingFields
|
||||
accessKey, owner, apiErr := getReqAccessKeyV2(r)
|
||||
if apiErr != ErrNone {
|
||||
return cred, apiErr
|
||||
}
|
||||
|
||||
cred = globalServerConfig.GetCredential()
|
||||
// Access credentials.
|
||||
cred := globalServerConfig.GetCredential()
|
||||
if keySignFields[0] != cred.AccessKey {
|
||||
return ErrInvalidAccessKeyID
|
||||
if !owner {
|
||||
if globalIAMSys == nil {
|
||||
return cred, ErrInvalidAccessKeyID
|
||||
}
|
||||
var ok bool
|
||||
cred, ok = globalIAMSys.GetUser(accessKey)
|
||||
if !ok {
|
||||
return cred, ErrInvalidAccessKeyID
|
||||
}
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
return cred, ErrNone
|
||||
}
|
||||
|
||||
func doesSignV2Match(r *http.Request) APIErrorCode {
|
||||
v2Auth := r.Header.Get("Authorization")
|
||||
|
||||
if apiError := validateV2AuthHeader(v2Auth); apiError != ErrNone {
|
||||
cred, apiError := validateV2AuthHeader(r)
|
||||
if apiError != ErrNone {
|
||||
return apiError
|
||||
}
|
||||
|
||||
@@ -249,12 +290,12 @@ func doesSignV2Match(r *http.Request) APIErrorCode {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("%s %s:", signV2Algorithm, globalServerConfig.GetCredential().AccessKey)
|
||||
prefix := fmt.Sprintf("%s %s:", signV2Algorithm, cred.AccessKey)
|
||||
if !strings.HasPrefix(v2Auth, prefix) {
|
||||
return ErrSignatureDoesNotMatch
|
||||
}
|
||||
v2Auth = v2Auth[len(prefix):]
|
||||
expectedAuth := signatureV2(r.Method, encodedResource, strings.Join(unescapedQueries, "&"), r.Header)
|
||||
expectedAuth := signatureV2(cred, r.Method, encodedResource, strings.Join(unescapedQueries, "&"), r.Header)
|
||||
if !compareSignatureV2(v2Auth, expectedAuth) {
|
||||
return ErrSignatureDoesNotMatch
|
||||
}
|
||||
@@ -268,15 +309,13 @@ func calculateSignatureV2(stringToSign string, secret string) string {
|
||||
}
|
||||
|
||||
// Return signature-v2 for the presigned request.
|
||||
func preSignatureV2(method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string {
|
||||
cred := globalServerConfig.GetCredential()
|
||||
func preSignatureV2(cred auth.Credentials, method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string {
|
||||
stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, expires)
|
||||
return calculateSignatureV2(stringToSign, cred.SecretKey)
|
||||
}
|
||||
|
||||
// Return the signature v2 of a given request.
|
||||
func signatureV2(method string, encodedResource string, encodedQuery string, headers http.Header) string {
|
||||
cred := globalServerConfig.GetCredential()
|
||||
func signatureV2(cred auth.Credentials, method string, encodedResource string, encodedQuery string, headers http.Header) string {
|
||||
stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, "")
|
||||
signature := calculateSignatureV2(stringToSign, cred.SecretKey)
|
||||
return signature
|
||||
|
||||
@@ -223,7 +223,12 @@ func TestValidateV2AuthHeader(t *testing.T) {
|
||||
for i, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("Case %d AuthStr \"%s\".", i+1, testCase.authString), func(t *testing.T) {
|
||||
|
||||
actualErrCode := validateV2AuthHeader(testCase.authString)
|
||||
req := &http.Request{
|
||||
Header: make(http.Header),
|
||||
URL: &url.URL{},
|
||||
}
|
||||
req.Header.Set("Authorization", testCase.authString)
|
||||
_, actualErrCode := validateV2AuthHeader(req)
|
||||
|
||||
if testCase.expectedError != actualErrCode {
|
||||
t.Errorf("Expected the error code to be %v, got %v.", testCase.expectedError, actualErrCode)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -46,6 +47,24 @@ func (c credentialHeader) getScope() string {
|
||||
}, "/")
|
||||
}
|
||||
|
||||
func getReqAccessKeyV4(r *http.Request, region string) (string, bool, APIErrorCode) {
|
||||
ch, err := parseCredentialHeader("Credential="+r.URL.Query().Get("X-Amz-Credential"), region)
|
||||
if err != ErrNone {
|
||||
// Strip off the Algorithm prefix.
|
||||
v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm)
|
||||
authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
|
||||
if len(authFields) != 3 {
|
||||
return "", false, ErrMissingFields
|
||||
}
|
||||
ch, err = parseCredentialHeader(authFields[0], region)
|
||||
if err != ErrNone {
|
||||
return "", false, err
|
||||
}
|
||||
}
|
||||
owner, s3Err := checkKeyValid(ch.accessKey)
|
||||
return ch.accessKey, owner, s3Err
|
||||
}
|
||||
|
||||
// parse credentialHeader string into its structured form.
|
||||
func parseCredentialHeader(credElement string, region string) (ch credentialHeader, aec APIErrorCode) {
|
||||
creds := strings.Split(strings.TrimSpace(credElement), "=")
|
||||
|
||||
@@ -100,6 +100,23 @@ func isValidRegion(reqRegion string, confRegion string) bool {
|
||||
return reqRegion == confRegion
|
||||
}
|
||||
|
||||
// check if the access key is valid and recognized, additionally
|
||||
// also returns if the access key is owner/admin.
|
||||
func checkKeyValid(accessKey string) (bool, APIErrorCode) {
|
||||
var owner = true
|
||||
if globalServerConfig.GetCredential().AccessKey != accessKey {
|
||||
if globalIAMSys == nil {
|
||||
return false, ErrInvalidAccessKeyID
|
||||
}
|
||||
// Check if the access key is part of users credentials.
|
||||
if _, ok := globalIAMSys.GetUser(accessKey); !ok {
|
||||
return false, ErrInvalidAccessKeyID
|
||||
}
|
||||
owner = false
|
||||
}
|
||||
return owner, ErrNone
|
||||
}
|
||||
|
||||
// sumHMAC calculate hmac between two input byte array.
|
||||
func sumHMAC(key []byte, data []byte) []byte {
|
||||
hash := hmac.New(sha256.New, key)
|
||||
|
||||
@@ -175,7 +175,14 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode {
|
||||
|
||||
// Verify if the access key id matches.
|
||||
if credHeader.accessKey != cred.AccessKey {
|
||||
return ErrInvalidAccessKeyID
|
||||
if globalIAMSys == nil {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
var ok bool
|
||||
cred, ok = globalIAMSys.GetUser(credHeader.accessKey)
|
||||
if !ok {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
}
|
||||
|
||||
// Get signing key.
|
||||
@@ -211,7 +218,14 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region s
|
||||
|
||||
// Verify if the access key id matches.
|
||||
if pSignValues.Credential.accessKey != cred.AccessKey {
|
||||
return ErrInvalidAccessKeyID
|
||||
if globalIAMSys == nil {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
var ok bool
|
||||
cred, ok = globalIAMSys.GetUser(pSignValues.Credential.accessKey)
|
||||
if !ok {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all the signed headers along with its values.
|
||||
@@ -335,7 +349,14 @@ func doesSignatureMatch(hashedPayload string, r *http.Request, region string) AP
|
||||
|
||||
// Verify if the access key id matches.
|
||||
if signV4Values.Credential.accessKey != cred.AccessKey {
|
||||
return ErrInvalidAccessKeyID
|
||||
if globalIAMSys == nil {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
var ok bool
|
||||
cred, ok = globalIAMSys.GetUser(signV4Values.Credential.accessKey)
|
||||
if !ok {
|
||||
return ErrInvalidAccessKeyID
|
||||
}
|
||||
}
|
||||
|
||||
// Extract date, if not present throw error.
|
||||
|
||||
@@ -64,7 +64,7 @@ func getChunkSignature(cred auth.Credentials, seedSignature string, region strin
|
||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
|
||||
// returns signature, error otherwise if the signature mismatches or any other
|
||||
// error while parsing and validating.
|
||||
func calculateSeedSignature(cred auth.Credentials, r *http.Request) (signature string, region string, date time.Time, errCode APIErrorCode) {
|
||||
func calculateSeedSignature(r *http.Request) (cred auth.Credentials, signature string, region string, date time.Time, errCode APIErrorCode) {
|
||||
// Copy request.
|
||||
req := *r
|
||||
|
||||
@@ -74,7 +74,7 @@ func calculateSeedSignature(cred auth.Credentials, r *http.Request) (signature s
|
||||
// Parse signature version '4' header.
|
||||
signV4Values, errCode := parseSignV4(v4Auth, globalServerConfig.GetRegion())
|
||||
if errCode != ErrNone {
|
||||
return "", "", time.Time{}, errCode
|
||||
return cred, "", "", time.Time{}, errCode
|
||||
}
|
||||
|
||||
// Payload streaming.
|
||||
@@ -82,17 +82,26 @@ func calculateSeedSignature(cred auth.Credentials, r *http.Request) (signature s
|
||||
|
||||
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
|
||||
if payload != req.Header.Get("X-Amz-Content-Sha256") {
|
||||
return "", "", time.Time{}, ErrContentSHA256Mismatch
|
||||
return cred, "", "", time.Time{}, ErrContentSHA256Mismatch
|
||||
}
|
||||
|
||||
// Extract all the signed headers along with its values.
|
||||
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
|
||||
if errCode != ErrNone {
|
||||
return "", "", time.Time{}, errCode
|
||||
return cred, "", "", time.Time{}, errCode
|
||||
}
|
||||
|
||||
cred = globalServerConfig.GetCredential()
|
||||
// Verify if the access key id matches.
|
||||
if signV4Values.Credential.accessKey != cred.AccessKey {
|
||||
return "", "", time.Time{}, ErrInvalidAccessKeyID
|
||||
if globalIAMSys == nil {
|
||||
return cred, "", "", time.Time{}, ErrInvalidAccessKeyID
|
||||
}
|
||||
var ok bool
|
||||
cred, ok = globalIAMSys.GetUser(signV4Values.Credential.accessKey)
|
||||
if !ok {
|
||||
return cred, "", "", time.Time{}, ErrInvalidAccessKeyID
|
||||
}
|
||||
}
|
||||
|
||||
// Verify if region is valid.
|
||||
@@ -102,14 +111,14 @@ func calculateSeedSignature(cred auth.Credentials, r *http.Request) (signature s
|
||||
var dateStr string
|
||||
if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" {
|
||||
if dateStr = r.Header.Get("Date"); dateStr == "" {
|
||||
return "", "", time.Time{}, ErrMissingDateHeader
|
||||
return cred, "", "", time.Time{}, ErrMissingDateHeader
|
||||
}
|
||||
}
|
||||
// Parse date header.
|
||||
var err error
|
||||
date, err = time.Parse(iso8601Format, dateStr)
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, ErrMalformedDate
|
||||
return cred, "", "", time.Time{}, ErrMalformedDate
|
||||
}
|
||||
|
||||
// Query string.
|
||||
@@ -129,11 +138,11 @@ func calculateSeedSignature(cred auth.Credentials, r *http.Request) (signature s
|
||||
|
||||
// Verify if signature match.
|
||||
if !compareSignatureV4(newSignature, signV4Values.Signature) {
|
||||
return "", "", time.Time{}, ErrSignatureDoesNotMatch
|
||||
return cred, "", "", time.Time{}, ErrSignatureDoesNotMatch
|
||||
}
|
||||
|
||||
// Return caculated signature.
|
||||
return newSignature, region, date, ErrNone
|
||||
return cred, newSignature, region, date, ErrNone
|
||||
}
|
||||
|
||||
const maxLineLength = 4 * humanize.KiByte // assumed <= bufio.defaultBufSize 4KiB
|
||||
@@ -151,10 +160,7 @@ var errMalformedEncoding = errors.New("malformed chunked encoding")
|
||||
// NewChunkedReader is not needed by normal applications. The http package
|
||||
// automatically decodes chunking when reading response bodies.
|
||||
func newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, APIErrorCode) {
|
||||
// Access credentials.
|
||||
cred := globalServerConfig.GetCredential()
|
||||
|
||||
seedSignature, region, seedDate, errCode := calculateSeedSignature(cred, req)
|
||||
cred, seedSignature, region, seedDate, errCode := calculateSeedSignature(req)
|
||||
if errCode != ErrNone {
|
||||
return nil, errCode
|
||||
}
|
||||
|
||||
119
cmd/sts-errors.go
Normal file
119
cmd/sts-errors.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// writeSTSErrorRespone writes error headers
|
||||
func writeSTSErrorResponse(w http.ResponseWriter, errorCode STSErrorCode) {
|
||||
stsError := getSTSError(errorCode)
|
||||
// Generate error response.
|
||||
stsErrorResponse := getSTSErrorResponse(stsError)
|
||||
encodedErrorResponse := encodeResponse(stsErrorResponse)
|
||||
writeResponse(w, stsError.HTTPStatusCode, encodedErrorResponse, mimeXML)
|
||||
}
|
||||
|
||||
// STSError structure
|
||||
type STSError struct {
|
||||
Code string
|
||||
Description string
|
||||
HTTPStatusCode int
|
||||
}
|
||||
|
||||
// STSErrorResponse - error response format
|
||||
type STSErrorResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ ErrorResponse" json:"-"`
|
||||
Error struct {
|
||||
Type string `xml:"Type"`
|
||||
Code string `xml:"Code"`
|
||||
Message string `xml:"Message"`
|
||||
} `xml:"Error"`
|
||||
RequestID string `xml:"RequestId"`
|
||||
}
|
||||
|
||||
// STSErrorCode type of error status.
|
||||
type STSErrorCode int
|
||||
|
||||
// Error codes, non exhaustive list - http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html
|
||||
const (
|
||||
ErrSTSNone STSErrorCode = iota
|
||||
ErrSTSMissingParameter
|
||||
ErrSTSInvalidParameterValue
|
||||
ErrSTSClientGrantsExpiredToken
|
||||
ErrSTSInvalidClientGrantsToken
|
||||
ErrSTSMalformedPolicyDocument
|
||||
ErrSTSNotInitialized
|
||||
ErrSTSInternalError
|
||||
)
|
||||
|
||||
// error code to STSError structure, these fields carry respective
|
||||
// descriptions for all the error responses.
|
||||
var stsErrCodeResponse = map[STSErrorCode]STSError{
|
||||
ErrSTSMissingParameter: {
|
||||
Code: "MissingParameter",
|
||||
Description: "A required parameter for the specified action is not supplied.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSInvalidParameterValue: {
|
||||
Code: "InvalidParameterValue",
|
||||
Description: "An invalid or out-of-range value was supplied for the input parameter.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSClientGrantsExpiredToken: {
|
||||
Code: "ExpiredToken",
|
||||
Description: "The client grants that was passed is expired or is not valid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSInvalidClientGrantsToken: {
|
||||
Code: "InvalidClientGrantsToken",
|
||||
Description: "The client grants token that was passed could not be validated by Minio.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSMalformedPolicyDocument: {
|
||||
Code: "MalformedPolicyDocument",
|
||||
Description: "The request was rejected because the policy document was malformed.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSNotInitialized: {
|
||||
Code: "STSNotInitialized",
|
||||
Description: "STS API not initialized, please try again.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrSTSInternalError: {
|
||||
Code: "InternalError",
|
||||
Description: "We encountered an internal error generating credentials, please try again.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
// getSTSError provides STS Error for input STS error code.
|
||||
func getSTSError(code STSErrorCode) STSError {
|
||||
return stsErrCodeResponse[code]
|
||||
}
|
||||
|
||||
// getErrorResponse gets in standard error and resource value and
|
||||
// provides a encodable populated response values
|
||||
func getSTSErrorResponse(err STSError) STSErrorResponse {
|
||||
errRsp := STSErrorResponse{}
|
||||
errRsp.Error.Code = err.Code
|
||||
errRsp.Error.Message = err.Description
|
||||
errRsp.RequestID = "3L137"
|
||||
return errRsp
|
||||
}
|
||||
203
cmd/sts-handlers.go
Normal file
203
cmd/sts-handlers.go
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/iam/validator"
|
||||
)
|
||||
|
||||
const (
|
||||
// STS API version.
|
||||
stsAPIVersion = "2011-06-15"
|
||||
)
|
||||
|
||||
// stsAPIHandlers implements and provides http handlers for AWS STS API.
|
||||
type stsAPIHandlers struct{}
|
||||
|
||||
// registerSTSRouter - registers AWS STS compatible APIs.
|
||||
func registerSTSRouter(router *mux.Router) {
|
||||
// Initialize STS.
|
||||
sts := &stsAPIHandlers{}
|
||||
|
||||
// STS Router
|
||||
stsRouter := router.NewRoute().PathPrefix("/").Subrouter()
|
||||
|
||||
// AssumeRoleWithClientGrants
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
|
||||
Queries("Action", "AssumeRoleWithClientGrants").
|
||||
Queries("Token", "{Token:.*}")
|
||||
|
||||
}
|
||||
|
||||
// AssumedRoleUser - The identifiers for the temporary security credentials that
|
||||
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
|
||||
type AssumedRoleUser struct {
|
||||
// The ARN of the temporary security credentials that are returned from the
|
||||
// AssumeRole action. For more information about ARNs and how to use them in
|
||||
// policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html)
|
||||
// in Using IAM.
|
||||
//
|
||||
// Arn is a required field
|
||||
Arn string
|
||||
|
||||
// A unique identifier that contains the role ID and the role session name of
|
||||
// the role that is being assumed. The role ID is generated by AWS when the
|
||||
// role is created.
|
||||
//
|
||||
// AssumedRoleId is a required field
|
||||
AssumedRoleID string `xml:"AssumeRoleId"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
|
||||
// AssumeRoleWithClientGrantsResponse contains the result of successful AssumeRoleWithClientGrants request.
|
||||
type AssumeRoleWithClientGrantsResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"`
|
||||
Result ClientGrantsResult `xml:"AssumeRoleWithClientGrantsResult"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// ClientGrantsResult - Contains the response to a successful AssumeRoleWithClientGrants
|
||||
// request, including temporary credentials that can be used to make Minio API requests.
|
||||
type ClientGrantsResult struct {
|
||||
// The identifiers for the temporary security credentials that the operation
|
||||
// returns.
|
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
|
||||
|
||||
// The intended audience (also known as client ID) of the web identity token.
|
||||
// This is traditionally the client identifier issued to the application that
|
||||
// requested the client grants.
|
||||
Audience string `xml:",omitempty"`
|
||||
|
||||
// The temporary security credentials, which include an access key ID, a secret
|
||||
// access key, and a security (or session) token.
|
||||
//
|
||||
// Note: The size of the security token that STS APIs return is not fixed. We
|
||||
// strongly recommend that you make no assumptions about the maximum size. As
|
||||
// of this writing, the typical size is less than 4096 bytes, but that can vary.
|
||||
// Also, future updates to AWS might require larger sizes.
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
|
||||
// A percentage value that indicates the size of the policy in packed form.
|
||||
// The service rejects any policy with a packed size greater than 100 percent,
|
||||
// which means the policy exceeded the allowed space.
|
||||
PackedPolicySize int `xml:",omitempty"`
|
||||
|
||||
// The issuing authority of the web identity token presented. For OpenID Connect
|
||||
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
|
||||
// this contains the value of the ProviderId parameter that was passed in the
|
||||
// AssumeRoleWithClientGrants request.
|
||||
Provider string `xml:",omitempty"`
|
||||
|
||||
// The unique user identifier that is returned by the identity provider.
|
||||
// This identifier is associated with the Token that was submitted
|
||||
// with the AssumeRoleWithClientGrants call. The identifier is typically unique to
|
||||
// the user and the application that acquired the ClientGrantsToken (pairwise identifier).
|
||||
// For OpenID Connect ID tokens, this field contains the value returned by the identity
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromToken string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
// AssumeRoleWithClientGrants - implementation of AWS STS extension API supporting
|
||||
// OAuth2.0 client grants.
|
||||
//
|
||||
// Eg:-
|
||||
// $ curl https://minio:9000/?Action=AssumeRoleWithClientGrants&Token=<jwt>
|
||||
func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AssumeRoleWithClientGrants")
|
||||
|
||||
if globalIAMValidators == nil {
|
||||
writeSTSErrorResponse(w, ErrSTSNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: this API only accepts JWT tokens.
|
||||
v, err := globalIAMValidators.Get("jwt")
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
return
|
||||
}
|
||||
|
||||
policyStr := r.URL.Query().Get("Policy")
|
||||
var p *iampolicy.Policy
|
||||
if policyStr != "" {
|
||||
var data []byte
|
||||
data, err = base64.URLEncoding.DecodeString(policyStr)
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
return
|
||||
}
|
||||
p, err = iampolicy.ParseConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
m, err := v.Validate(vars["Token"], r.URL.Query().Get("DurationSeconds"))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case validator.ErrTokenExpired:
|
||||
writeSTSErrorResponse(w, ErrSTSClientGrantsExpiredToken)
|
||||
case validator.ErrInvalidDuration:
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
secret := globalServerConfig.GetCredential().SecretKey
|
||||
cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the newly generated credentials.
|
||||
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInternalError)
|
||||
return
|
||||
}
|
||||
if p != nil {
|
||||
if err = globalIAMSys.SetPolicy(cred.AccessKey, *p); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInternalError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodedSuccessResponse := encodeResponse(&AssumeRoleWithClientGrantsResponse{
|
||||
Result: ClientGrantsResult{Credentials: cred},
|
||||
})
|
||||
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
}
|
||||
@@ -354,10 +354,16 @@ func UnstartedTestServer(t TestErrHandler, instanceType string) TestServer {
|
||||
globalMinioPort = port
|
||||
globalMinioAddr = getEndpointsLocalAddr(testServer.Disks)
|
||||
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, testServer.Disks)
|
||||
globalConfigSys = NewConfigSys()
|
||||
|
||||
globalIAMSys = NewIAMSys()
|
||||
globalIAMSys.Init(objLayer)
|
||||
|
||||
// Create new policy system.
|
||||
globalPolicySys = NewPolicySys()
|
||||
globalPolicySys.Init(objLayer)
|
||||
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, testServer.Disks)
|
||||
globalNotificationSys.Init(objLayer)
|
||||
|
||||
return testServer
|
||||
}
|
||||
@@ -495,7 +501,7 @@ func resetTestGlobals() {
|
||||
// Configure the server for the test run.
|
||||
func newTestConfig(bucketLocation string, obj ObjectLayer) (err error) {
|
||||
// Initialize server config.
|
||||
if err = newConfig(obj); err != nil {
|
||||
if err = newSrvConfig(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1620,11 +1626,13 @@ func newTestObjectLayer(endpoints EndpointList) (newObject ObjectLayer, err erro
|
||||
return xl.storageDisks
|
||||
}
|
||||
|
||||
// Create new notification system.
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, endpoints)
|
||||
globalConfigSys = NewConfigSys()
|
||||
|
||||
globalIAMSys = NewIAMSys()
|
||||
globalIAMSys.Init(xl)
|
||||
|
||||
// Create new policy system.
|
||||
globalPolicySys = NewPolicySys()
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, endpoints)
|
||||
|
||||
return xl, nil
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
@@ -236,10 +237,11 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
|
||||
if web.CacheAPI() != nil {
|
||||
listBuckets = web.CacheAPI().ListBuckets
|
||||
}
|
||||
authErr := webRequestAuthenticate(r)
|
||||
if authErr != nil {
|
||||
|
||||
if _, _, authErr := webRequestAuthenticate(r); authErr != nil {
|
||||
return toJSONError(authErr)
|
||||
}
|
||||
|
||||
// If etcd, dns federation configured list buckets from etcd.
|
||||
if globalDNSConfig != nil {
|
||||
dnsBuckets, err := globalDNSConfig.List()
|
||||
@@ -305,32 +307,66 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
|
||||
if objectAPI == nil {
|
||||
return toJSONError(errServerNotInitialized)
|
||||
}
|
||||
|
||||
listObjects := objectAPI.ListObjects
|
||||
if web.CacheAPI() != nil {
|
||||
listObjects = web.CacheAPI().ListObjects
|
||||
}
|
||||
|
||||
// Check if anonymous (non-owner) has access to download objects.
|
||||
readable := globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
// Check if anonymous (non-owner) has access to upload objects.
|
||||
writable := globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
claims, owner, authErr := webRequestAuthenticate(r)
|
||||
if authErr != nil {
|
||||
if authErr == errNoAuthToken {
|
||||
// Check if anonymous (non-owner) has access to download objects.
|
||||
readable := globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
|
||||
if authErr := webRequestAuthenticate(r); authErr != nil {
|
||||
if authErr == errAuthentication {
|
||||
// Check if anonymous (non-owner) has access to upload objects.
|
||||
writable := globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
|
||||
reply.Writable = writable
|
||||
if !readable {
|
||||
// Error out if anonymous user (non-owner) has no access to download or upload objects
|
||||
if !writable {
|
||||
return errAuthentication
|
||||
}
|
||||
// return empty object list if access is write only
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return toJSONError(authErr)
|
||||
}
|
||||
}
|
||||
|
||||
// For authenticated users apply IAM policy.
|
||||
if authErr == nil {
|
||||
readable := globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.Action(policy.GetObjectAction),
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: owner,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
|
||||
writable := globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.Action(policy.PutObjectAction),
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: owner,
|
||||
ObjectName: args.Prefix + "/",
|
||||
})
|
||||
|
||||
reply.Writable = writable
|
||||
if !readable {
|
||||
@@ -341,7 +377,6 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
|
||||
// return empty object list if access is write only
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
lo, err := listObjects(context.Background(), args.BucketName, args.Prefix, args.Marker, slashSeparator, 1000)
|
||||
@@ -619,18 +654,34 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
if authErr := webRequestAuthenticate(r); authErr != nil {
|
||||
if authErr == errAuthentication {
|
||||
writeWebErrorResponse(w, errAuthentication)
|
||||
claims, owner, authErr := webRequestAuthenticate(r)
|
||||
if authErr != nil {
|
||||
if authErr == errNoAuthToken {
|
||||
// Check if anonymous (non-owner) has access to upload objects.
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
writeWebErrorResponse(w, errAuthentication)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
writeWebErrorResponse(w, authErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if anonymous (non-owner) has access to upload objects.
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.PutObjectAction,
|
||||
// For authenticated users apply IAM policy.
|
||||
if authErr == nil {
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.Action(policy.PutObjectAction),
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
IsOwner: owner,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
writeWebErrorResponse(w, errAuthentication)
|
||||
@@ -734,13 +785,34 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
object := vars["object"]
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
if !isAuthTokenValid(token) {
|
||||
// Check if anonymous (non-owner) has access to download objects.
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
claims, owner, authErr := webTokenAuthenticate(token)
|
||||
if authErr != nil {
|
||||
if authErr == errNoAuthToken {
|
||||
// Check if anonymous (non-owner) has access to download objects.
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
writeWebErrorResponse(w, errAuthentication)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
writeWebErrorResponse(w, authErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For authenticated users apply IAM policy.
|
||||
if authErr == nil {
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.Action(policy.GetObjectAction),
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
IsOwner: owner,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
writeWebErrorResponse(w, errAuthentication)
|
||||
@@ -886,14 +958,7 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
writeWebErrorResponse(w, errServerNotInitialized)
|
||||
return
|
||||
}
|
||||
getObject := objectAPI.GetObject
|
||||
if web.CacheAPI() != nil {
|
||||
getObject = web.CacheAPI().GetObject
|
||||
}
|
||||
listObjects := objectAPI.ListObjects
|
||||
if web.CacheAPI() != nil {
|
||||
listObjects = web.CacheAPI().ListObjects
|
||||
}
|
||||
|
||||
// Auth is done after reading the body to accommodate for anonymous requests
|
||||
// when bucket policy is enabled.
|
||||
var args DownloadZipArgs
|
||||
@@ -905,14 +970,37 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
token := r.URL.Query().Get("token")
|
||||
if !isAuthTokenValid(token) {
|
||||
claims, owner, authErr := webTokenAuthenticate(token)
|
||||
if authErr != nil {
|
||||
if authErr == errNoAuthToken {
|
||||
for _, object := range args.Objects {
|
||||
// Check if anonymous (non-owner) has access to download objects.
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
ObjectName: pathJoin(args.Prefix, object),
|
||||
}) {
|
||||
writeWebErrorResponse(w, errAuthentication)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeWebErrorResponse(w, authErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For authenticated users apply IAM policy.
|
||||
if authErr == nil {
|
||||
for _, object := range args.Objects {
|
||||
// Check if anonymous (non-owner) has access to download objects.
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.GetObjectAction,
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.Action(policy.GetObjectAction),
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
IsOwner: owner,
|
||||
ObjectName: pathJoin(args.Prefix, object),
|
||||
}) {
|
||||
writeWebErrorResponse(w, errAuthentication)
|
||||
@@ -921,8 +1009,18 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
getObject := objectAPI.GetObject
|
||||
if web.CacheAPI() != nil {
|
||||
getObject = web.CacheAPI().GetObject
|
||||
}
|
||||
listObjects := objectAPI.ListObjects
|
||||
if web.CacheAPI() != nil {
|
||||
listObjects = web.CacheAPI().ListObjects
|
||||
}
|
||||
|
||||
archive := zip.NewWriter(w)
|
||||
defer archive.Close()
|
||||
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
if web.CacheAPI() != nil {
|
||||
getObjectInfo = web.CacheAPI().GetObjectInfo
|
||||
|
||||
@@ -1294,7 +1294,7 @@ func testWebGetBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestE
|
||||
},
|
||||
}
|
||||
|
||||
if err = savePolicyConfig(obj, bucketName, bucketPolicy); err != nil {
|
||||
if err = savePolicyConfig(context.Background(), obj, bucketName, bucketPolicy); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
@@ -1394,7 +1394,7 @@ func testWebListAllBucketPoliciesHandler(obj ObjectLayer, instanceType string, t
|
||||
},
|
||||
}
|
||||
|
||||
if err = savePolicyConfig(obj, bucketName, bucketPolicy); err != nil {
|
||||
if err = savePolicyConfig(context.Background(), obj, bucketName, bucketPolicy); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -499,7 +499,7 @@ func (s *xlSets) ListObjectsV2(ctx context.Context, bucket, prefix, continuation
|
||||
|
||||
// SetBucketPolicy persist the new policy on the bucket.
|
||||
func (s *xlSets) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
|
||||
return savePolicyConfig(s, bucket, policy)
|
||||
return savePolicyConfig(ctx, s, bucket, policy)
|
||||
}
|
||||
|
||||
// GetBucketPolicy will return a policy on a bucket
|
||||
|
||||
@@ -277,7 +277,7 @@ func (xl xlObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
|
||||
// SetBucketPolicy sets policy on bucket
|
||||
func (xl xlObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
|
||||
return savePolicyConfig(xl, bucket, policy)
|
||||
return savePolicyConfig(ctx, xl, bucket, policy)
|
||||
}
|
||||
|
||||
// GetBucketPolicy will get policy on bucket
|
||||
|
||||
Reference in New Issue
Block a user