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 disksmaster
parent
16a100b597
commit
54ae364def
@ -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 |
||||
} |
@ -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), |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -0,0 +1,46 @@ |
||||
# Minio multi-user Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) |
||||
This document explains how to add, revoke users. Multi-user as name implies means Minio supports long term users other than default credentials, each of these users can be configured to deny or allow access to buckets, resources. |
||||
|
||||
## Get started |
||||
In this document we will explain in detail on how to configure multiple users. |
||||
|
||||
### 1. Prerequisites |
||||
- Install mc - [Minio Client Quickstart Guide](https://docs.minio.io/docs/minio-client-quickstart-guide.html) |
||||
- Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide) |
||||
|
||||
### 2. Create a new user and policy |
||||
Create a new user `newuser` on Minio use `mc admin users`, with a `newuser.json`. |
||||
``` |
||||
mc admin users add myminio newuser newuser123 /tmp/newuser.json |
||||
``` |
||||
|
||||
An example user policy, enables `newuser` to download all objects in my-bucketname. |
||||
```json |
||||
{ |
||||
"Version": "2012-10-17", |
||||
"Statement": [ |
||||
{ |
||||
"Action": [ |
||||
"s3:GetObject" |
||||
], |
||||
"Effect": "Allow", |
||||
"Resource": [ |
||||
"arn:aws:s3:::my-bucketname/*" |
||||
], |
||||
"Sid": "" |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
### 3. Revoke user |
||||
Temporarily revoke access for `newuser`. |
||||
``` |
||||
mc admin users revoke myminio newuser |
||||
``` |
||||
|
||||
### 4. Remove user |
||||
Remove the user `newuser`. |
||||
``` |
||||
mc admin users remove myminio newuser |
||||
``` |
@ -0,0 +1,51 @@ |
||||
# Minio STS Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) |
||||
The Minio Security Token Service (STS) is an endpoint service that enables clients to request temporary credentials for Minio resources. Temporary credentials work almost identically to default admin credentials, with some differences: |
||||
|
||||
- Temporary credentials are short-term, as the name implies. They can be configured to last for anywhere from a few minutes to several hours. After the credentials expire, Minio no longer recognizes them or allows any kind of access from API requests made with them. |
||||
- Temporary credentials do not need to be stored with the application but are generated dynamically and provided to the application when requested. When (or even before) the temporary credentials expire, the application can request new credentials. |
||||
|
||||
Following are advantages for using temporary credentials: |
||||
|
||||
- No need embed long-term credentials with an application. |
||||
- No need to provide access to buckets and objects without having to define static credentials. |
||||
- Temporary credentials have a limited lifetime, no need to rotate them or explicitly revoke them when they're no longer needed. After temporary credentials expire, they cannot be reused. |
||||
|
||||
## Identity Federation |
||||
[**Client grants**](./client-grants.md) - Let applications request `client_grants` using any well-known third party identity provider such as KeyCloak, WSO2. This is known as the client grants approach to temporary access. Using this approach helps clients keep Minio credentials to be secured. Minio STS client grants supports WSO2, Keycloak. |
||||
|
||||
## Get started |
||||
In this document we will explain in detail on how to configure all the prerequisites, primarily WSO2, OPA (open policy agent). |
||||
|
||||
### 1. Prerequisites |
||||
- [Configuring wso2](./wso2.md) |
||||
- [Configuring opa](./opa.md) |
||||
|
||||
### 2. Setup Minio with WSO2, OPA |
||||
Make sure we have followed the previous step and configured each software independently, once done we can now proceed to use Minio STS API and Minio server to use these credentials to perform object API operations. |
||||
|
||||
``` |
||||
export MINIO_ACCESS_KEY=minio |
||||
export MINIO_SECRET_KEY=minio123 |
||||
export MINIO_IAM_JWKS_URL=https://localhost:9443/oauth2/jwks |
||||
export MINIO_IAM_OPA_URL=http://localhost:8181/v1/data/httpapi/authz |
||||
minio server /mnt/data |
||||
``` |
||||
|
||||
### 3. Test using full-example.go |
||||
On another terminal run `full-example.go` a sample client application which obtains JWT access tokens from an identity provider, in our case its WSO2. Uses the returned access token response to get new temporary credentials from the Minio server using the STS API call `AssumeRoleWithClientGrants`. |
||||
|
||||
``` |
||||
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga |
||||
|
||||
##### Credentials |
||||
{ |
||||
"accessKey": "NUIBORZYTV2HG2BMRSXR", |
||||
"secretKey": "qQlP5O7CFPc5m5IXf1vYhuVTFj7BRVJqh0FqZ86S", |
||||
"expiration": "2018-08-21T17:10:29-07:00", |
||||
"sessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJOVUlCT1JaWVRWMkhHMkJNUlNYUiIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTM0ODk2NjI5LCJpYXQiOjE1MzQ4OTMwMjksImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiNjY2OTZjZTctN2U1Ny00ZjU5LWI0MWQtM2E1YTMzZGZiNjA4In0.eJONnVaSVHypiXKEARSMnSKgr-2mlC2Sr4fEGJitLcJF_at3LeNdTHv0_oHsv6ZZA3zueVGgFlVXMlREgr9LXA" |
||||
} |
||||
``` |
||||
|
||||
## Explore Further |
||||
- [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide) |
||||
- [The Minio documentation website](https://docs.minio.io) |
@ -0,0 +1,71 @@ |
||||
## AssumeRoleWithClientGrants [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) |
||||
Returns a set of temporary security credentials for applications/clients who have been authenticated through client grants provided by identity provider. Example providers include WSO2, KeyCloak etc. |
||||
|
||||
Calling AssumeRoleWithClientGrants does not require the use of Minio default credentials. Therefore, client application can be distributed that requests temporary security credentials without including Minio default credentials. Instead, the identity of the caller is validated by using a JWT access token from the identity provider. The temporary security credentials returned by this API consist of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations. |
||||
|
||||
By default, the temporary security credentials created by AssumeRoleWithClientGrants last for one hour. However, use the optional DurationSeconds parameter to specify the duration of the credentials. This value varies from 900 seconds (15 minutes) up to the maximum session duration to 12 hours. |
||||
|
||||
### Request Parameters |
||||
#### DurationSeconds |
||||
The duration, in seconds. The value can range from 900 seconds (15 minutes) up to the 12 hours. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds. |
||||
|
||||
| Params | Value | |
||||
| :-- | :-- | |
||||
| *Type* | *Integer* | |
||||
| *Valid Range* | *Minimum value of 900. Maximum value of 43200.* | |
||||
| *Required* | *No* | |
||||
|
||||
#### Token |
||||
The OAuth 2.0 access token that is provided by the identity provider. Application must get this token by authenticating the application using client grants before the application makes an AssumeRoleWithClientGrants call. |
||||
|
||||
| Params | Value | |
||||
| :-- | :-- | |
||||
| *Type* | *String* | |
||||
| *Length Constraints* | *Minimum length of 4. Maximum length of 2048.* | |
||||
| *Required* | *Yes* | |
||||
|
||||
#### Response Elements |
||||
XML response for this API is similar to [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_ResponseElements) |
||||
|
||||
#### Errors |
||||
XML error response for this API is similar to [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html#API_AssumeRoleWithWebIdentity_Errors) |
||||
|
||||
#### Testing |
||||
``` |
||||
$ export MINIO_ACCESS_KEY=minio |
||||
$ export MINIO_SECRET_KEY=minio123 |
||||
$ export MINIO_IAM_JWKS_URL=https://localhost:9443/oauth2/jwks |
||||
$ export MINIO_IAM_OPA_URL=http://localhost:8181/v1/data/httpapi/authz |
||||
$ minio server /mnt/export |
||||
|
||||
$ mc admin config get myminio |
||||
... |
||||
{ |
||||
"openid": { |
||||
"jwks": { |
||||
"url": "https://localhost:9443/oauth2/jwks" |
||||
} |
||||
} |
||||
"policy": { |
||||
"opa": { |
||||
"url": "http://localhost:8181/v1/data/httpapi/authz", |
||||
"authToken": "" |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
Testing with an example |
||||
> Obtaining client ID and secrets follow [WSO2 configuring documentation](./wso2.md) |
||||
|
||||
``` |
||||
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga |
||||
|
||||
##### Credentials |
||||
{ |
||||
"accessKey": "NUIBORZYTV2HG2BMRSXR", |
||||
"secretKey": "qQlP5O7CFPc5m5IXf1vYhuVTFj7BRVJqh0FqZ86S", |
||||
"expiration": "2018-08-21T17:10:29-07:00", |
||||
"sessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJOVUlCT1JaWVRWMkhHMkJNUlNYUiIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTM0ODk2NjI5LCJpYXQiOjE1MzQ4OTMwMjksImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiNjY2OTZjZTctN2U1Ny00ZjU5LWI0MWQtM2E1YTMzZGZiNjA4In0.eJONnVaSVHypiXKEARSMnSKgr-2mlC2Sr4fEGJitLcJF_at3LeNdTHv0_oHsv6ZZA3zueVGgFlVXMlREgr9LXA" |
||||
} |
||||
``` |
@ -0,0 +1,17 @@ |
||||
version: '2' |
||||
services: |
||||
opa: |
||||
image: openpolicyagent/opa:0.9.1 |
||||
ports: |
||||
- 8181:8181 |
||||
command: |
||||
- "run" |
||||
- "--server" |
||||
- "--log-level=debug" |
||||
api_server: |
||||
image: openpolicyagent/demo-restful-api:0.2 |
||||
ports: |
||||
- 5000:5000 |
||||
environment: |
||||
- OPA_ADDR=http://opa:8181 |
||||
- POLICY_PATH=/v1/data/httpapi/authz |
@ -0,0 +1,180 @@ |
||||
/* |
||||
* 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 main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/tls" |
||||
"encoding/json" |
||||
"encoding/xml" |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
|
||||
minio "github.com/minio/minio-go" |
||||
"github.com/minio/minio-go/pkg/credentials" |
||||
"github.com/minio/minio/pkg/auth" |
||||
) |
||||
|
||||
// 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 { |
||||
Arn string |
||||
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 { |
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"` |
||||
Audience string `xml:",omitempty"` |
||||
Credentials auth.Credentials `xml:",omitempty"` |
||||
PackedPolicySize int `xml:",omitempty"` |
||||
Provider string `xml:",omitempty"` |
||||
SubjectFromClientGrantsToken string `xml:",omitempty"` |
||||
} |
||||
|
||||
// JWTToken - parses the output from IDP access token.
|
||||
type JWTToken struct { |
||||
AccessToken string `json:"access_token"` |
||||
Expiry int `json:"expires_in"` |
||||
} |
||||
|
||||
var ( |
||||
stsEndpoint string |
||||
idpEndpoint string |
||||
clientID string |
||||
clientSecret string |
||||
) |
||||
|
||||
func init() { |
||||
flag.StringVar(&stsEndpoint, "sts-ep", "http://localhost:9000", "STS endpoint") |
||||
flag.StringVar(&idpEndpoint, "idp-ep", "https://localhost:9443/oauth2/token", "IDP endpoint") |
||||
flag.StringVar(&clientID, "cid", "", "Client ID") |
||||
flag.StringVar(&clientSecret, "csec", "", "Client secret") |
||||
} |
||||
|
||||
func main() { |
||||
flag.Parse() |
||||
if clientID == "" || clientSecret == "" { |
||||
flag.PrintDefaults() |
||||
return |
||||
} |
||||
|
||||
data := url.Values{} |
||||
data.Set("grant_type", "client_credentials") |
||||
req, err := http.NewRequest(http.MethodPost, idpEndpoint, strings.NewReader(data.Encode())) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
||||
req.SetBasicAuth(clientID, clientSecret) |
||||
t := &http.Transport{ |
||||
TLSClientConfig: &tls.Config{ |
||||
InsecureSkipVerify: true, |
||||
}, |
||||
} |
||||
hclient := http.Client{ |
||||
Transport: t, |
||||
} |
||||
resp, err := hclient.Do(req) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
defer resp.Body.Close() |
||||
if resp.StatusCode != http.StatusOK { |
||||
log.Fatal(resp.Status) |
||||
} |
||||
|
||||
var idpToken JWTToken |
||||
if err = json.NewDecoder(resp.Body).Decode(&idpToken); err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
v := url.Values{} |
||||
v.Set("Action", "AssumeRoleWithClientGrants") |
||||
v.Set("Token", idpToken.AccessToken) |
||||
v.Set("DurationSeconds", fmt.Sprintf("%d", idpToken.Expiry)) |
||||
|
||||
u, err := url.Parse(stsEndpoint) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
u.RawQuery = v.Encode() |
||||
|
||||
req, err = http.NewRequest("POST", u.String(), nil) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
resp, err = http.DefaultClient.Do(req) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
defer resp.Body.Close() |
||||
if resp.StatusCode != http.StatusOK { |
||||
log.Fatal(resp.Status) |
||||
} |
||||
|
||||
a := AssumeRoleWithClientGrantsResponse{} |
||||
if err = xml.NewDecoder(resp.Body).Decode(&a); err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
fmt.Println("##### Credentials") |
||||
c, err := json.MarshalIndent(a.Result.Credentials, "", "\t") |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
fmt.Println(string(c)) |
||||
|
||||
// Uncommend this to use Minio API operations by initializin minio
|
||||
// client with obtained credentials.
|
||||
|
||||
opts := &minio.Options{ |
||||
Creds: credentials.NewStaticV4(a.Result.Credentials.AccessKey, |
||||
a.Result.Credentials.SecretKey, |
||||
a.Result.Credentials.SessionToken, |
||||
), |
||||
BucketLookup: minio.BucketLookupAuto, |
||||
} |
||||
|
||||
clnt, err := minio.NewWithOptions(u.Host, opts) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
d := bytes.NewReader([]byte("Hello, World")) |
||||
n, err := clnt.PutObject("my-bucketname", "my-objectname", d, d.Size(), minio.PutObjectOptions{}) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
log.Println("Uploaded", "my-objectname", " of size: ", n, "Successfully.") |
||||
} |
@ -0,0 +1,85 @@ |
||||
# OPA Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) |
||||
OPA is a lightweight general-purpose policy engine that can be co-located with Minio server, in this document we talk about how to use OPA HTTP API to authorize Minio STS credentials. |
||||
|
||||
## Get started |
||||
### 1. Prerequisites |
||||
- Docker 18.03 or above, refer here for [installation](https://docs.docker.com/install/). |
||||
- Docker compose 1.20 or above, refere here for [installation](https://docs.docker.com/compose/install/#prerequisites). |
||||
|
||||
### 2. Start OPA |
||||
First, create a `docker-compose.yml` file that runs OPA and the demo web server. |
||||
``` |
||||
cat >docker-compose.yml <<EOF |
||||
version: '2' |
||||
services: |
||||
opa: |
||||
image: openpolicyagent/opa:0.9.1 |
||||
ports: |
||||
- 8181:8181 |
||||
command: |
||||
- "run" |
||||
- "--server" |
||||
- "--log-level=debug" |
||||
api_server: |
||||
image: openpolicyagent/demo-restful-api:0.2 |
||||
ports: |
||||
- 5000:5000 |
||||
environment: |
||||
- OPA_ADDR=http://opa:8181 |
||||
- POLICY_PATH=/v1/data/httpapi/authz |
||||
EOF |
||||
``` |
||||
|
||||
Then run `docker-compose` to pull and run the containers. |
||||
``` |
||||
docker-compose -f docker-compose.yml up |
||||
``` |
||||
|
||||
### 3. Create new OPA Policy |
||||
In another terminal, create a policy that allows users to upload objects |
||||
``` |
||||
cat > putobject.rego <<EOF |
||||
package httpapi.authz |
||||
|
||||
import input as http_api |
||||
|
||||
allow { |
||||
input.action = "s3:PutObject" |
||||
input.owner = false |
||||
} |
||||
|
||||
EOF |
||||
``` |
||||
|
||||
Then load the policy via OPA's REST API. |
||||
``` |
||||
curl -X PUT --data-binary @putobject.rego \ |
||||
localhost:8181/v1/policies/putobject |
||||
``` |
||||
|
||||
### 4. Setup Minio with OPA |
||||
Minio server expects environment variable for OPA http API url as `MINIO_IAM_OPA_URL`, this environment variable takes a single entry. |
||||
``` |
||||
export MINIO_IAM_OPA_URL=http://localhost:8181/v1/data/httpapi/authz |
||||
minio server /mnt/data |
||||
``` |
||||
|
||||
### 5. Test with Minio STS API |
||||
Assuming that Minio server is configured to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide), execute the following command to temporary credentials from Minio server. |
||||
``` |
||||
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga |
||||
|
||||
##### Credentials |
||||
{ |
||||
"accessKey": "IRBLVDGN5QGMDCMO1X8V", |
||||
"secretKey": "KzS3UZKE7xqNdtRbKyfcWgxBS6P1G4kwZn4DXKuY", |
||||
"expiration": "2018-08-21T15:49:38-07:00", |
||||
"sessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJJUkJMVkRHTjVRR01EQ01PMVg4ViIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTM0ODkxNzc4LCJpYXQiOjE1MzQ4ODgxNzgsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiMTg0NDMyOWMtZDY1YS00OGEzLTgyMjgtOWRmNzNmZTgzZDU2In0.4rKsZ8VkZnIS_ALzfTJ9UbEKPFlQVvIyuHw6AWTJcDFDVgQA2ooQHmH9wUDnhXBi1M7o8yWJ47DXP-TLPhwCgQ" |
||||
} |
||||
``` |
||||
|
||||
These credentials can now be used to perform Minio API operations, these credentials automatically expire in 1hr. To understand more about credential expiry duration and client grants STS API read further [here](https://docs.minio.io/docs/api-assume-role-with-client-grants). |
||||
|
||||
## Explore Further |
||||
- [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide) |
||||
- [The Minio documentation website](https://docs.minio.io) |
@ -0,0 +1,4 @@ |
||||
export MINIO_ACCESS_KEY=minio |
||||
export MINIO_SECRET_KEY=minio123 |
||||
export MINIO_IAM_JWKS_URL=http://localhost:9763/oauth2/jwks |
||||
export MINIO_IAM_OPA_URL=http://localhost:8181/v1/data/httpapi/authz |
@ -0,0 +1,88 @@ |
||||
# WSO2 Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) |
||||
WSO2 is an Identity Server open source and is released under Apache Software License Version 2.0, this document covers configuring WSO2 to be used as an identity provider for Minio server STS API. |
||||
|
||||
## Get started |
||||
### 1. Prerequisites |
||||
- JAVA 1.8 and above installed already and JAVA_HOME points to JAVA 1.8 installation. |
||||
- Download WSO2 follow their [installation guide](https://docs.wso2.com/display/IS540/Installation+Guide). |
||||
|
||||
### 2. Configure WSO2 |
||||
Once WSO2 is up and running, configure WSO2 to generate Self contained access tokens. In OAuth 2.0 specification there are primarily two ways to provide access tokens |
||||
|
||||
1. The access token is an identifier that is hard to guess. For example, a randomly generated string of sufficient length, that the server handling the protected resource can use to lookup the associated authorization information. |
||||
2. The access token self-contains the authorization information in a manner that can be verified. For example, by encoding authorization information along with a signature into the token. |
||||
|
||||
WSO2 generates tokens in first style by default, but if to be used with Minio we should configure WSO2 to provide JWT tokens instead. |
||||
|
||||
### 3. Generate Self-contained Access Tokens |
||||
By default, a UUID is issued as an access token in WSO2 Identity Server, which is of the first type above. But, it also can be configured to issue a self-contained access token (JWT), which is of the second type above. |
||||
|
||||
- Open the `<IS_HOME>/repository/conf/identity/identity.xml` file and uncomment the following entry under `<OAuth>` element. |
||||
``` |
||||
<IdentityOAuthTokenGenerator>org.wso2.carbon.identity.oauth2.token.JWTTokenIssuer</IdentityOAuthTokenGenerator> |
||||
``` |
||||
- Restart the server. |
||||
- Configure an [OAuth service provider](https://docs.wso2.com/display/IS540/Adding+and+Configuring+a+Service+Provider). |
||||
- Initiate an access token request to the WSO2 Identity Server, over a known [grant type](https://docs.wso2.com/display/IS540/OAuth+2.0+Grant+Types). For example, the following cURL command illustrates the syntax of an access token request that can be initiated over the [Client Credentials Grant](https://docs.wso2.com/display/IS540/Client+Credentials+Grant) grant type. |
||||
- Navigate to service provider section, expand Inbound Authentication Configurations and expand OAuth/OpenID Connect Configuration. |
||||
- Copy the OAuth Client Key as the value for `<CLIENT_ID>`. |
||||
- Copy the OAuth Client Secret as the value for `<CLIENT_SECRET>`. |
||||
- By default, `<IS_HOST>` is localhost. However, if using a public IP, the respective IP address or domain needs to be specified. |
||||
- By default, `<IS_HTTPS_PORT>` has been set to 9443. However, if the port offset has been incremented by n, the default port value needs to be incremented by n. |
||||
|
||||
Request |
||||
``` |
||||
curl -u <CLIENT_ID>:<CLIENT_SECRET> -k -d "grant_type=client_credentials" -H "Content-Type:application/x-www-form-urlencoded" https://<IS_HOST>:<IS_HTTPS_PORT>/oauth2/token |
||||
``` |
||||
|
||||
Example: |
||||
``` |
||||
curl -u PoEgXP6uVO45IsENRngDXj5Au5Ya:eKsw6z8CtOJVBtrOWvhRWL4TUCga -k -d "grant_type=client_credentials" -H "Content-Type:application/x-www-form-urlencoded" https://localhost:9443/oauth2/token |
||||
``` |
||||
|
||||
In response, the self-contained JWT access token will be returned as shown below. |
||||
``` |
||||
{ |
||||
"access_token": "eyJ4NXQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJraWQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiYXpwIjoiUG9FZ1hQNnVWTzQ1SXNFTlJuZ0RYajVBdTVZYSIsImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyXC90b2tlbiIsImV4cCI6MTUzNDg5MTc3OCwiaWF0IjoxNTM0ODg4MTc4LCJqdGkiOiIxODQ0MzI5Yy1kNjVhLTQ4YTMtODIyOC05ZGY3M2ZlODNkNTYifQ.ELZ8ujk2Xp9xTGgMqnCa5ehuimaAPXWlSCW5QeBbTJIT4M5OB_2XEVIV6p89kftjUdKu50oiYe4SbfrxmLm6NGSGd2qxkjzJK3SRKqsrmVWEn19juj8fz1neKtUdXVHuSZu6ws_bMDy4f_9hN2Jv9dFnkoyeNT54r4jSTJ4A2FzN2rkiURheVVsc8qlm8O7g64Az-5h4UGryyXU4zsnjDCBKYk9jdbEpcUskrFMYhuUlj1RWSASiGhHHHDU5dTRqHkVLIItfG48k_fb-ehU60T7EFWH1JBdNjOxM9oN_yb0hGwOjLUyCUJO_Y7xcd5F4dZzrBg8LffFmvJ09wzHNtQ", |
||||
"token_type": "Bearer", |
||||
"expires_in": 3600 |
||||
} |
||||
``` |
||||
|
||||
### 4. JWT Claims |
||||
The access token received is a signed JSON Web Token (JWT). Use a JWT decoder to decode the access token to access the payload of the token that includes following JWT claims: |
||||
|
||||
|Claim Name|Type|Claim Value| |
||||
|:--:|:--:|:--:| |
||||
|iss| string | The issuer of the JWT. The '> Identity Provider Entity Id ' value of the OAuth2/OpenID Connect Inbound Authentication configuration of the Resident Identity Provider is returned here. | |
||||
|aud| string array | The token audience list. The client identifier of the OAuth clients that the JWT is intended for, is sent herewith. | |
||||
|azp| string | The authorized party for which the token is issued to. The client identifier of the OAuth client that the token is issued for, is sent herewith. | |
||||
|iat| integer | The token issue time. | |
||||
|exp| integer | The token expiration time. | |
||||
|jti| string | Unique identifier for the JWT token. | |
||||
|
||||
Using the above `access_token` we can perform an STS request to Minio to get temporary credentials for Minio API operations. Minio STS API uses [JSON Web Key Set Endpoint](https://docs.wso2.com/display/IS541/JSON+Web+Key+Set+Endpoint) to validate if JWT is valid and is properly signed. |
||||
|
||||
### 5. Setup Minio with JWKS URL |
||||
Minio server expects environment variable for JWKS url as `MINIO_IAM_JWKS_URL`, this environment variable takes a single entry. |
||||
``` |
||||
export MINIO_IAM_JWKS_URL=https://localhost:9443/oauth2/jwks |
||||
minio server /mnt/data |
||||
``` |
||||
|
||||
Assuming that Minio server is configured to support STS API by following the doc [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide), execute the following command to temporary credentials from Minio server. |
||||
``` |
||||
go run full-example.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga |
||||
|
||||
##### Credentials |
||||
{ |
||||
"accessKey": "IRBLVDGN5QGMDCMO1X8V", |
||||
"secretKey": "KzS3UZKE7xqNdtRbKyfcWgxBS6P1G4kwZn4DXKuY", |
||||
"expiration": "2018-08-21T15:49:38-07:00", |
||||
"sessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJJUkJMVkRHTjVRR01EQ01PMVg4ViIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTM0ODkxNzc4LCJpYXQiOjE1MzQ4ODgxNzgsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiMTg0NDMyOWMtZDY1YS00OGEzLTgyMjgtOWRmNzNmZTgzZDU2In0.4rKsZ8VkZnIS_ALzfTJ9UbEKPFlQVvIyuHw6AWTJcDFDVgQA2ooQHmH9wUDnhXBi1M7o8yWJ47DXP-TLPhwCgQ" |
||||
} |
||||
``` |
||||
|
||||
## Explore Further |
||||
- [Minio STS Quickstart Guide](https://docs.minio.io/docs/minio-sts-quickstart-guide) |
||||
- [The Minio documentation website](https://docs.minio.io) |
@ -0,0 +1,272 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
"github.com/minio/minio/pkg/policy/condition" |
||||
"github.com/minio/minio/pkg/wildcard" |
||||
) |
||||
|
||||
// Action - policy action.
|
||||
// Refer https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazons3.html
|
||||
// for more information about available actions.
|
||||
type Action string |
||||
|
||||
const ( |
||||
// AbortMultipartUploadAction - AbortMultipartUpload Rest API action.
|
||||
AbortMultipartUploadAction Action = "s3:AbortMultipartUpload" |
||||
|
||||
// CreateBucketAction - CreateBucket Rest API action.
|
||||
CreateBucketAction = "s3:CreateBucket" |
||||
|
||||
// DeleteBucketAction - DeleteBucket Rest API action.
|
||||
DeleteBucketAction = "s3:DeleteBucket" |
||||
|
||||
// DeleteBucketPolicyAction - DeleteBucketPolicy Rest API action.
|
||||
DeleteBucketPolicyAction = "s3:DeleteBucketPolicy" |
||||
|
||||
// DeleteObjectAction - DeleteObject Rest API action.
|
||||
DeleteObjectAction = "s3:DeleteObject" |
||||
|
||||
// GetBucketLocationAction - GetBucketLocation Rest API action.
|
||||
GetBucketLocationAction = "s3:GetBucketLocation" |
||||
|
||||
// GetBucketNotificationAction - GetBucketNotification Rest API action.
|
||||
GetBucketNotificationAction = "s3:GetBucketNotification" |
||||
|
||||
// GetBucketPolicyAction - GetBucketPolicy Rest API action.
|
||||
GetBucketPolicyAction = "s3:GetBucketPolicy" |
||||
|
||||
// GetObjectAction - GetObject Rest API action.
|
||||
GetObjectAction = "s3:GetObject" |
||||
|
||||
// HeadBucketAction - HeadBucket Rest API action. This action is unused in minio.
|
||||
HeadBucketAction = "s3:HeadBucket" |
||||
|
||||
// ListAllMyBucketsAction - ListAllMyBuckets (List buckets) Rest API action.
|
||||
ListAllMyBucketsAction = "s3:ListAllMyBuckets" |
||||
|
||||
// ListBucketAction - ListBucket Rest API action.
|
||||
ListBucketAction = "s3:ListBucket" |
||||
|
||||
// ListBucketMultipartUploadsAction - ListMultipartUploads Rest API action.
|
||||
ListBucketMultipartUploadsAction = "s3:ListBucketMultipartUploads" |
||||
|
||||
// ListenBucketNotificationAction - ListenBucketNotification Rest API action.
|
||||
// This is Minio extension.
|
||||
ListenBucketNotificationAction = "s3:ListenBucketNotification" |
||||
|
||||
// ListMultipartUploadPartsAction - ListParts Rest API action.
|
||||
ListMultipartUploadPartsAction = "s3:ListMultipartUploadParts" |
||||
|
||||
// PutBucketNotificationAction - PutObjectNotification Rest API action.
|
||||
PutBucketNotificationAction = "s3:PutBucketNotification" |
||||
|
||||
// PutBucketPolicyAction - PutBucketPolicy Rest API action.
|
||||
PutBucketPolicyAction = "s3:PutBucketPolicy" |
||||
|
||||
// PutObjectAction - PutObject Rest API action.
|
||||
PutObjectAction = "s3:PutObject" |
||||
|
||||
// AllActions - all API actions
|
||||
AllActions = "s3:*" |
||||
) |
||||
|
||||
// List of all supported actions.
|
||||
var supportedActions = map[Action]struct{}{ |
||||
AllActions: {}, |
||||
AbortMultipartUploadAction: {}, |
||||
CreateBucketAction: {}, |
||||
DeleteBucketAction: {}, |
||||
DeleteBucketPolicyAction: {}, |
||||
DeleteObjectAction: {}, |
||||
GetBucketLocationAction: {}, |
||||
GetBucketNotificationAction: {}, |
||||
GetBucketPolicyAction: {}, |
||||
GetObjectAction: {}, |
||||
HeadBucketAction: {}, |
||||
ListAllMyBucketsAction: {}, |
||||
ListBucketAction: {}, |
||||
ListBucketMultipartUploadsAction: {}, |
||||
ListenBucketNotificationAction: {}, |
||||
ListMultipartUploadPartsAction: {}, |
||||
PutBucketNotificationAction: {}, |
||||
PutBucketPolicyAction: {}, |
||||
PutObjectAction: {}, |
||||
} |
||||
|
||||
// isObjectAction - returns whether action is object type or not.
|
||||
func (action Action) isObjectAction() bool { |
||||
switch action { |
||||
case AbortMultipartUploadAction, DeleteObjectAction, GetObjectAction: |
||||
fallthrough |
||||
case ListMultipartUploadPartsAction, PutObjectAction, AllActions: |
||||
return true |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
// Match - matches object name with resource pattern.
|
||||
func (action Action) Match(a Action) bool { |
||||
return wildcard.Match(string(action), string(a)) |
||||
} |
||||
|
||||
// IsValid - checks if action is valid or not.
|
||||
func (action Action) IsValid() bool { |
||||
_, ok := supportedActions[action] |
||||
return ok |
||||
} |
||||
|
||||
// MarshalJSON - encodes Action to JSON data.
|
||||
func (action Action) MarshalJSON() ([]byte, error) { |
||||
if action.IsValid() { |
||||
return json.Marshal(string(action)) |
||||
} |
||||
|
||||
return nil, fmt.Errorf("invalid action '%v'", action) |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data to Action.
|
||||
func (action *Action) UnmarshalJSON(data []byte) error { |
||||
var s string |
||||
|
||||
if err := json.Unmarshal(data, &s); err != nil { |
||||
return err |
||||
} |
||||
|
||||
a := Action(s) |
||||
if !a.IsValid() { |
||||
return fmt.Errorf("invalid action '%v'", s) |
||||
} |
||||
|
||||
*action = a |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func parseAction(s string) (Action, error) { |
||||
action := Action(s) |
||||
|
||||
if action.IsValid() { |
||||
return action, nil |
||||
} |
||||
|
||||
return action, fmt.Errorf("unsupported action '%v'", s) |
||||
} |
||||
|
||||
// actionConditionKeyMap - holds mapping of supported condition key for an action.
|
||||
var actionConditionKeyMap = map[Action]condition.KeySet{ |
||||
AbortMultipartUploadAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
CreateBucketAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
DeleteBucketPolicyAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
DeleteObjectAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
GetBucketLocationAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
GetBucketNotificationAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
GetBucketPolicyAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
GetObjectAction: condition.NewKeySet( |
||||
condition.S3XAmzServerSideEncryption, |
||||
condition.S3XAmzServerSideEncryptionAwsKMSKeyID, |
||||
condition.S3XAmzStorageClass, |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
HeadBucketAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
ListAllMyBucketsAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
ListBucketAction: condition.NewKeySet( |
||||
condition.S3Prefix, |
||||
condition.S3Delimiter, |
||||
condition.S3MaxKeys, |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
ListBucketMultipartUploadsAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
ListenBucketNotificationAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
ListMultipartUploadPartsAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
PutBucketNotificationAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
PutBucketPolicyAction: condition.NewKeySet( |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
|
||||
PutObjectAction: condition.NewKeySet( |
||||
condition.S3XAmzCopySource, |
||||
condition.S3XAmzServerSideEncryption, |
||||
condition.S3XAmzServerSideEncryptionAwsKMSKeyID, |
||||
condition.S3XAmzMetadataDirective, |
||||
condition.S3XAmzStorageClass, |
||||
condition.AWSReferer, |
||||
condition.AWSSourceIP, |
||||
), |
||||
} |
@ -0,0 +1,116 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func TestActionIsObjectAction(t *testing.T) { |
||||
testCases := []struct { |
||||
action Action |
||||
expectedResult bool |
||||
}{ |
||||
{AbortMultipartUploadAction, true}, |
||||
{DeleteObjectAction, true}, |
||||
{GetObjectAction, true}, |
||||
{ListMultipartUploadPartsAction, true}, |
||||
{PutObjectAction, true}, |
||||
{CreateBucketAction, false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.action.isObjectAction() |
||||
|
||||
if testCase.expectedResult != result { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionIsValid(t *testing.T) { |
||||
testCases := []struct { |
||||
action Action |
||||
expectedResult bool |
||||
}{ |
||||
{AbortMultipartUploadAction, true}, |
||||
{Action("foo"), false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.action.IsValid() |
||||
|
||||
if testCase.expectedResult != result { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionMarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
action Action |
||||
expectedResult []byte |
||||
expectErr bool |
||||
}{ |
||||
{PutObjectAction, []byte(`"s3:PutObject"`), false}, |
||||
{Action("foo"), nil, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result, err := json.Marshal(testCase.action) |
||||
expectErr := (err != nil) |
||||
|
||||
if testCase.expectErr != expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionUnmarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
data []byte |
||||
expectedResult Action |
||||
expectErr bool |
||||
}{ |
||||
{[]byte(`"s3:PutObject"`), PutObjectAction, false}, |
||||
{[]byte(`"foo"`), Action(""), true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
var result Action |
||||
err := json.Unmarshal(testCase.data, &result) |
||||
expectErr := (err != nil) |
||||
|
||||
if testCase.expectErr != expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if testCase.expectedResult != result { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"sort" |
||||
|
||||
"github.com/minio/minio-go/pkg/set" |
||||
) |
||||
|
||||
// ActionSet - set of actions.
|
||||
type ActionSet map[Action]struct{} |
||||
|
||||
// Add - add action to the set.
|
||||
func (actionSet ActionSet) Add(action Action) { |
||||
actionSet[action] = struct{}{} |
||||
} |
||||
|
||||
// Match - matches object name with anyone of action pattern in action set.
|
||||
func (actionSet ActionSet) Match(action Action) bool { |
||||
for r := range actionSet { |
||||
if r.Match(action) { |
||||
return true |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
// Intersection - returns actions available in both ActionSet.
|
||||
func (actionSet ActionSet) Intersection(sset ActionSet) ActionSet { |
||||
nset := NewActionSet() |
||||
for k := range actionSet { |
||||
if _, ok := sset[k]; ok { |
||||
nset.Add(k) |
||||
} |
||||
} |
||||
|
||||
return nset |
||||
} |
||||
|
||||
// MarshalJSON - encodes ActionSet to JSON data.
|
||||
func (actionSet ActionSet) MarshalJSON() ([]byte, error) { |
||||
if len(actionSet) == 0 { |
||||
return nil, fmt.Errorf("empty action set") |
||||
} |
||||
|
||||
return json.Marshal(actionSet.ToSlice()) |
||||
} |
||||
|
||||
func (actionSet ActionSet) String() string { |
||||
actions := []string{} |
||||
for action := range actionSet { |
||||
actions = append(actions, string(action)) |
||||
} |
||||
sort.Strings(actions) |
||||
|
||||
return fmt.Sprintf("%v", actions) |
||||
} |
||||
|
||||
// ToSlice - returns slice of actions from the action set.
|
||||
func (actionSet ActionSet) ToSlice() []Action { |
||||
actions := []Action{} |
||||
for action := range actionSet { |
||||
actions = append(actions, action) |
||||
} |
||||
|
||||
return actions |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data to ActionSet.
|
||||
func (actionSet *ActionSet) UnmarshalJSON(data []byte) error { |
||||
var sset set.StringSet |
||||
if err := json.Unmarshal(data, &sset); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if len(sset) == 0 { |
||||
return fmt.Errorf("empty action set") |
||||
} |
||||
|
||||
*actionSet = make(ActionSet) |
||||
for _, s := range sset.ToSlice() { |
||||
action, err := parseAction(s) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
actionSet.Add(action) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// NewActionSet - creates new action set.
|
||||
func NewActionSet(actions ...Action) ActionSet { |
||||
actionSet := make(ActionSet) |
||||
for _, action := range actions { |
||||
actionSet.Add(action) |
||||
} |
||||
|
||||
return actionSet |
||||
} |
@ -0,0 +1,159 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func TestActionSetAdd(t *testing.T) { |
||||
testCases := []struct { |
||||
set ActionSet |
||||
action Action |
||||
expectedResult ActionSet |
||||
}{ |
||||
{NewActionSet(), PutObjectAction, NewActionSet(PutObjectAction)}, |
||||
{NewActionSet(PutObjectAction), PutObjectAction, NewActionSet(PutObjectAction)}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
testCase.set.Add(testCase.action) |
||||
|
||||
if !reflect.DeepEqual(testCase.expectedResult, testCase.set) { |
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionSetMatches(t *testing.T) { |
||||
testCases := []struct { |
||||
set ActionSet |
||||
action Action |
||||
expectedResult bool |
||||
}{ |
||||
{NewActionSet(AllActions), AbortMultipartUploadAction, true}, |
||||
{NewActionSet(PutObjectAction), PutObjectAction, true}, |
||||
{NewActionSet(PutObjectAction, GetObjectAction), PutObjectAction, true}, |
||||
{NewActionSet(PutObjectAction, GetObjectAction), AbortMultipartUploadAction, false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.set.Match(testCase.action) |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionSetIntersection(t *testing.T) { |
||||
testCases := []struct { |
||||
set ActionSet |
||||
setToIntersect ActionSet |
||||
expectedResult ActionSet |
||||
}{ |
||||
{NewActionSet(), NewActionSet(PutObjectAction), NewActionSet()}, |
||||
{NewActionSet(PutObjectAction), NewActionSet(), NewActionSet()}, |
||||
{NewActionSet(PutObjectAction), NewActionSet(PutObjectAction, GetObjectAction), NewActionSet(PutObjectAction)}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.set.Intersection(testCase.setToIntersect) |
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionSetMarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
actionSet ActionSet |
||||
expectedResult []byte |
||||
expectErr bool |
||||
}{ |
||||
{NewActionSet(PutObjectAction), []byte(`["s3:PutObject"]`), false}, |
||||
{NewActionSet(), nil, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result, err := json.Marshal(testCase.actionSet) |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, string(testCase.expectedResult), string(result)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionSetToSlice(t *testing.T) { |
||||
testCases := []struct { |
||||
actionSet ActionSet |
||||
expectedResult []Action |
||||
}{ |
||||
{NewActionSet(PutObjectAction), []Action{PutObjectAction}}, |
||||
{NewActionSet(), []Action{}}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.actionSet.ToSlice() |
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestActionSetUnmarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
data []byte |
||||
expectedResult ActionSet |
||||
expectErr bool |
||||
}{ |
||||
{[]byte(`"s3:PutObject"`), NewActionSet(PutObjectAction), false}, |
||||
{[]byte(`["s3:PutObject"]`), NewActionSet(PutObjectAction), false}, |
||||
{[]byte(`["s3:PutObject", "s3:GetObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false}, |
||||
{[]byte(`["s3:PutObject", "s3:GetObject", "s3:PutObject"]`), NewActionSet(PutObjectAction, GetObjectAction), false}, |
||||
{[]byte(`[]`), NewActionSet(), true}, // Empty array.
|
||||
{[]byte(`"foo"`), nil, true}, // Invalid action.
|
||||
{[]byte(`["s3:PutObject", "foo"]`), nil, true}, // Invalid action.
|
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := make(ActionSet) |
||||
err := json.Unmarshal(testCase.data, &result) |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,172 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/tls" |
||||
"encoding/json" |
||||
"net" |
||||
"net/http" |
||||
"os" |
||||
"time" |
||||
|
||||
xnet "github.com/minio/minio/pkg/net" |
||||
) |
||||
|
||||
// OpaArgs opa general purpose policy engine configuration.
|
||||
type OpaArgs struct { |
||||
URL *xnet.URL `json:"url"` |
||||
AuthToken string `json:"authToken"` |
||||
} |
||||
|
||||
// Validate - validate opa configuration params.
|
||||
func (a *OpaArgs) Validate() error { |
||||
return nil |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data.
|
||||
func (a *OpaArgs) UnmarshalJSON(data []byte) error { |
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subOpaArgs OpaArgs |
||||
var so subOpaArgs |
||||
|
||||
if opaURL, ok := os.LookupEnv("MINIO_IAM_OPA_URL"); ok { |
||||
u, err := xnet.ParseURL(opaURL) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
so.URL = u |
||||
so.AuthToken = os.Getenv("MINIO_IAM_OPA_AUTHTOKEN") |
||||
} else { |
||||
if err := json.Unmarshal(data, &so); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
oa := OpaArgs(so) |
||||
if oa.URL == nil || oa.URL.String() == "" { |
||||
*a = oa |
||||
return nil |
||||
} |
||||
|
||||
if err := oa.Validate(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
*a = oa |
||||
return nil |
||||
} |
||||
|
||||
// Opa - implements opa policy agent calls.
|
||||
type Opa struct { |
||||
args OpaArgs |
||||
secureFailed bool |
||||
client *http.Client |
||||
insecureClient *http.Client |
||||
} |
||||
|
||||
// newCustomHTTPTransport returns a new http configuration
|
||||
// used while communicating with the cloud backends.
|
||||
// This sets the value for MaxIdleConnsPerHost from 2 (go default)
|
||||
// to 100.
|
||||
func newCustomHTTPTransport(insecure bool) *http.Transport { |
||||
return &http.Transport{ |
||||
Proxy: http.ProxyFromEnvironment, |
||||
DialContext: (&net.Dialer{ |
||||
Timeout: 30 * time.Second, |
||||
KeepAlive: 30 * time.Second, |
||||
}).DialContext, |
||||
MaxIdleConns: 1024, |
||||
MaxIdleConnsPerHost: 1024, |
||||
IdleConnTimeout: 30 * time.Second, |
||||
TLSHandshakeTimeout: 10 * time.Second, |
||||
ExpectContinueTimeout: 1 * time.Second, |
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, |
||||
DisableCompression: true, |
||||
} |
||||
} |
||||
|
||||
// NewOpa - initializes opa policy engine connector.
|
||||
func NewOpa(args OpaArgs) *Opa { |
||||
// No opa args.
|
||||
if args.URL == nil && args.AuthToken == "" { |
||||
return nil |
||||
} |
||||
return &Opa{ |
||||
args: args, |
||||
client: &http.Client{Transport: newCustomHTTPTransport(false)}, |
||||
insecureClient: &http.Client{Transport: newCustomHTTPTransport(true)}, |
||||
} |
||||
} |
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the REST API.
|
||||
func (o *Opa) IsAllowed(args Args) bool { |
||||
if o == nil { |
||||
return false |
||||
} |
||||
|
||||
// OPA input
|
||||
body := make(map[string]interface{}) |
||||
body["input"] = args |
||||
|
||||
inputBytes, err := json.Marshal(body) |
||||
if err != nil { |
||||
return false |
||||
} |
||||
|
||||
req, err := http.NewRequest("POST", o.args.URL.String(), bytes.NewReader(inputBytes)) |
||||
if err != nil { |
||||
return false |
||||
} |
||||
|
||||
req.Header.Set("Content-Type", "application/json") |
||||
if o.args.AuthToken != "" { |
||||
req.Header.Set("Authorization", o.args.AuthToken) |
||||
} |
||||
|
||||
var resp *http.Response |
||||
if o.secureFailed { |
||||
resp, err = o.insecureClient.Do(req) |
||||
} else { |
||||
resp, err = o.client.Do(req) |
||||
if err != nil { |
||||
o.secureFailed = true |
||||
resp, err = o.insecureClient.Do(req) |
||||
if err != nil { |
||||
return false |
||||
} |
||||
} |
||||
} |
||||
if err != nil { |
||||
return false |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
// Handle OPA response
|
||||
type opaResponse struct { |
||||
Result struct { |
||||
Allow bool `json:"allow"` |
||||
} `json:"result"` |
||||
} |
||||
var result opaResponse |
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { |
||||
return false |
||||
} |
||||
|
||||
return result.Result.Allow |
||||
} |
@ -0,0 +1,173 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
|
||||
"github.com/minio/minio/pkg/policy" |
||||
) |
||||
|
||||
// DefaultVersion - default policy version as per AWS S3 specification.
|
||||
const DefaultVersion = "2012-10-17" |
||||
|
||||
// Args - arguments to policy to check whether it is allowed
|
||||
type Args struct { |
||||
AccountName string `json:"account"` |
||||
Action Action `json:"action"` |
||||
BucketName string `json:"bucket"` |
||||
ConditionValues map[string][]string `json:"conditions"` |
||||
IsOwner bool `json:"owner"` |
||||
ObjectName string `json:"object"` |
||||
Claims map[string]interface{} `json:"claims"` |
||||
} |
||||
|
||||
// Policy - iam bucket iamp.
|
||||
type Policy struct { |
||||
ID policy.ID `json:"ID,omitempty"` |
||||
Version string |
||||
Statements []Statement `json:"Statement"` |
||||
} |
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
func (iamp Policy) IsAllowed(args Args) bool { |
||||
// Check all deny statements. If any one statement denies, return false.
|
||||
for _, statement := range iamp.Statements { |
||||
if statement.Effect == policy.Deny { |
||||
if !statement.IsAllowed(args) { |
||||
return false |
||||
} |
||||
} |
||||
} |
||||
|
||||
// For owner, its allowed by default.
|
||||
if args.IsOwner { |
||||
return true |
||||
} |
||||
|
||||
// Check all allow statements. If any one statement allows, return true.
|
||||
for _, statement := range iamp.Statements { |
||||
if statement.Effect == policy.Allow { |
||||
if statement.IsAllowed(args) { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
// IsEmpty - returns whether policy is empty or not.
|
||||
func (iamp Policy) IsEmpty() bool { |
||||
return len(iamp.Statements) == 0 |
||||
} |
||||
|
||||
// isValid - checks if Policy is valid or not.
|
||||
func (iamp Policy) isValid() error { |
||||
if iamp.Version != DefaultVersion && iamp.Version != "" { |
||||
return fmt.Errorf("invalid version '%v'", iamp.Version) |
||||
} |
||||
|
||||
for _, statement := range iamp.Statements { |
||||
if err := statement.isValid(); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
for i := range iamp.Statements { |
||||
for _, statement := range iamp.Statements[i+1:] { |
||||
actions := iamp.Statements[i].Actions.Intersection(statement.Actions) |
||||
if len(actions) == 0 { |
||||
continue |
||||
} |
||||
|
||||
resources := iamp.Statements[i].Resources.Intersection(statement.Resources) |
||||
if len(resources) == 0 { |
||||
continue |
||||
} |
||||
|
||||
if iamp.Statements[i].Conditions.String() != statement.Conditions.String() { |
||||
continue |
||||
} |
||||
|
||||
return fmt.Errorf("duplicate actions %v, resources %v found in statements %v, %v", |
||||
actions, resources, iamp.Statements[i], statement) |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// MarshalJSON - encodes Policy to JSON data.
|
||||
func (iamp Policy) MarshalJSON() ([]byte, error) { |
||||
if err := iamp.isValid(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// subtype to avoid recursive call to MarshalJSON()
|
||||
type subPolicy Policy |
||||
return json.Marshal(subPolicy(iamp)) |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data to Iamp.
|
||||
func (iamp *Policy) UnmarshalJSON(data []byte) error { |
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subPolicy Policy |
||||
var sp subPolicy |
||||
if err := json.Unmarshal(data, &sp); err != nil { |
||||
return err |
||||
} |
||||
|
||||
p := Policy(sp) |
||||
if err := p.isValid(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
*iamp = p |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Validate - validates all statements are for given bucket or not.
|
||||
func (iamp Policy) Validate() error { |
||||
if err := iamp.isValid(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, statement := range iamp.Statements { |
||||
if err := statement.Validate(); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// ParseConfig - parses data in given reader to Iamp.
|
||||
func ParseConfig(reader io.Reader) (*Policy, error) { |
||||
var iamp Policy |
||||
|
||||
decoder := json.NewDecoder(reader) |
||||
decoder.DisallowUnknownFields() |
||||
if err := decoder.Decode(&iamp); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &iamp, iamp.Validate() |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,129 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/minio/minio/pkg/wildcard" |
||||
) |
||||
|
||||
// ResourceARNPrefix - resource ARN prefix as per AWS S3 specification.
|
||||
const ResourceARNPrefix = "arn:aws:s3:::" |
||||
|
||||
// Resource - resource in policy statement.
|
||||
type Resource struct { |
||||
BucketName string |
||||
Pattern string |
||||
} |
||||
|
||||
func (r Resource) isBucketPattern() bool { |
||||
return !strings.Contains(r.Pattern, "/") || r.Pattern == "*" |
||||
} |
||||
|
||||
func (r Resource) isObjectPattern() bool { |
||||
return strings.Contains(r.Pattern, "/") || strings.Contains(r.BucketName, "*") || r.Pattern == "*/*" |
||||
} |
||||
|
||||
// IsValid - checks whether Resource is valid or not.
|
||||
func (r Resource) IsValid() bool { |
||||
return r.Pattern != "" |
||||
} |
||||
|
||||
// Match - matches object name with resource pattern.
|
||||
func (r Resource) Match(resource string) bool { |
||||
if strings.HasPrefix(resource, r.Pattern) { |
||||
return true |
||||
} |
||||
return wildcard.Match(r.Pattern, resource) |
||||
} |
||||
|
||||
// MarshalJSON - encodes Resource to JSON data.
|
||||
func (r Resource) MarshalJSON() ([]byte, error) { |
||||
if !r.IsValid() { |
||||
return nil, fmt.Errorf("invalid resource %v", r) |
||||
} |
||||
|
||||
return json.Marshal(r.String()) |
||||
} |
||||
|
||||
func (r Resource) String() string { |
||||
return ResourceARNPrefix + r.Pattern |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data to Resource.
|
||||
func (r *Resource) UnmarshalJSON(data []byte) error { |
||||
var s string |
||||
if err := json.Unmarshal(data, &s); err != nil { |
||||
return err |
||||
} |
||||
|
||||
parsedResource, err := parseResource(s) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
*r = parsedResource |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Validate - validates Resource is for given bucket or not.
|
||||
func (r Resource) Validate() error { |
||||
if !r.IsValid() { |
||||
return fmt.Errorf("invalid resource") |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// parseResource - parses string to Resource.
|
||||
func parseResource(s string) (Resource, error) { |
||||
if !strings.HasPrefix(s, ResourceARNPrefix) { |
||||
return Resource{}, fmt.Errorf("invalid resource '%v'", s) |
||||
} |
||||
|
||||
pattern := strings.TrimPrefix(s, ResourceARNPrefix) |
||||
tokens := strings.SplitN(pattern, "/", 2) |
||||
bucketName := tokens[0] |
||||
if bucketName == "" { |
||||
return Resource{}, fmt.Errorf("invalid resource format '%v'", s) |
||||
} |
||||
|
||||
return Resource{ |
||||
BucketName: bucketName, |
||||
Pattern: pattern, |
||||
}, nil |
||||
} |
||||
|
||||
// NewResource - creates new resource.
|
||||
func NewResource(bucketName, keyName string) Resource { |
||||
pattern := bucketName |
||||
if keyName != "" { |
||||
if !strings.HasPrefix(keyName, "/") { |
||||
pattern += "/" |
||||
} |
||||
|
||||
pattern += keyName |
||||
} |
||||
|
||||
return Resource{ |
||||
BucketName: bucketName, |
||||
Pattern: pattern, |
||||
} |
||||
} |
@ -0,0 +1,220 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func TestResourceIsBucketPattern(t *testing.T) { |
||||
testCases := []struct { |
||||
resource Resource |
||||
expectedResult bool |
||||
}{ |
||||
{NewResource("*", ""), true}, |
||||
{NewResource("mybucket", ""), true}, |
||||
{NewResource("mybucket*", ""), true}, |
||||
{NewResource("mybucket?0", ""), true}, |
||||
{NewResource("", "*"), false}, |
||||
{NewResource("*", "*"), false}, |
||||
{NewResource("mybucket", "*"), false}, |
||||
{NewResource("mybucket*", "/myobject"), false}, |
||||
{NewResource("mybucket?0", "/2010/photos/*"), false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.resource.isBucketPattern() |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceIsObjectPattern(t *testing.T) { |
||||
testCases := []struct { |
||||
resource Resource |
||||
expectedResult bool |
||||
}{ |
||||
{NewResource("*", ""), true}, |
||||
{NewResource("mybucket*", ""), true}, |
||||
{NewResource("", "*"), true}, |
||||
{NewResource("*", "*"), true}, |
||||
{NewResource("mybucket", "*"), true}, |
||||
{NewResource("mybucket*", "/myobject"), true}, |
||||
{NewResource("mybucket?0", "/2010/photos/*"), true}, |
||||
{NewResource("mybucket", ""), false}, |
||||
{NewResource("mybucket?0", ""), false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.resource.isObjectPattern() |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceIsValid(t *testing.T) { |
||||
testCases := []struct { |
||||
resource Resource |
||||
expectedResult bool |
||||
}{ |
||||
{NewResource("*", ""), true}, |
||||
{NewResource("mybucket*", ""), true}, |
||||
{NewResource("*", "*"), true}, |
||||
{NewResource("mybucket", "*"), true}, |
||||
{NewResource("mybucket*", "/myobject"), true}, |
||||
{NewResource("mybucket?0", "/2010/photos/*"), true}, |
||||
{NewResource("mybucket", ""), true}, |
||||
{NewResource("mybucket?0", ""), true}, |
||||
{NewResource("", "*"), true}, |
||||
{NewResource("", ""), false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.resource.IsValid() |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceMatch(t *testing.T) { |
||||
testCases := []struct { |
||||
resource Resource |
||||
objectName string |
||||
expectedResult bool |
||||
}{ |
||||
{NewResource("*", ""), "mybucket", true}, |
||||
{NewResource("*", ""), "mybucket/myobject", true}, |
||||
{NewResource("mybucket*", ""), "mybucket", true}, |
||||
{NewResource("mybucket*", ""), "mybucket/myobject", true}, |
||||
{NewResource("", "*"), "/myobject", true}, |
||||
{NewResource("*", "*"), "mybucket/myobject", true}, |
||||
{NewResource("mybucket", "*"), "mybucket/myobject", true}, |
||||
{NewResource("mybucket*", "/myobject"), "mybucket/myobject", true}, |
||||
{NewResource("mybucket*", "/myobject"), "mybucket100/myobject", true}, |
||||
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket20/2010/photos/1.jpg", true}, |
||||
{NewResource("mybucket", ""), "mybucket", true}, |
||||
{NewResource("mybucket?0", ""), "mybucket30", true}, |
||||
{NewResource("", "*"), "mybucket/myobject", false}, |
||||
{NewResource("*", "*"), "mybucket", false}, |
||||
{NewResource("mybucket", "*"), "mybucket10/myobject", false}, |
||||
{NewResource("mybucket?0", "/2010/photos/*"), "mybucket0/2010/photos/1.jpg", false}, |
||||
{NewResource("mybucket", ""), "mybucket/myobject", true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.resource.Match(testCase.objectName) |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceMarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
resource Resource |
||||
expectedResult []byte |
||||
expectErr bool |
||||
}{ |
||||
{NewResource("*", ""), []byte(`"arn:aws:s3:::*"`), false}, |
||||
{NewResource("mybucket*", ""), []byte(`"arn:aws:s3:::mybucket*"`), false}, |
||||
{NewResource("mybucket", ""), []byte(`"arn:aws:s3:::mybucket"`), false}, |
||||
{NewResource("*", "*"), []byte(`"arn:aws:s3:::*/*"`), false}, |
||||
{NewResource("", "*"), []byte(`"arn:aws:s3:::/*"`), false}, |
||||
{NewResource("mybucket", "*"), []byte(`"arn:aws:s3:::mybucket/*"`), false}, |
||||
{NewResource("mybucket*", "myobject"), []byte(`"arn:aws:s3:::mybucket*/myobject"`), false}, |
||||
{NewResource("mybucket?0", "/2010/photos/*"), []byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), false}, |
||||
{Resource{}, nil, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result, err := json.Marshal(testCase.resource) |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceUnmarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
data []byte |
||||
expectedResult Resource |
||||
expectErr bool |
||||
}{ |
||||
{[]byte(`"arn:aws:s3:::*"`), NewResource("*", ""), false}, |
||||
{[]byte(`"arn:aws:s3:::mybucket*"`), NewResource("mybucket*", ""), false}, |
||||
{[]byte(`"arn:aws:s3:::mybucket"`), NewResource("mybucket", ""), false}, |
||||
{[]byte(`"arn:aws:s3:::*/*"`), NewResource("*", "*"), false}, |
||||
{[]byte(`"arn:aws:s3:::mybucket/*"`), NewResource("mybucket", "*"), false}, |
||||
{[]byte(`"arn:aws:s3:::mybucket*/myobject"`), NewResource("mybucket*", "myobject"), false}, |
||||
{[]byte(`"arn:aws:s3:::mybucket?0/2010/photos/*"`), NewResource("mybucket?0", "/2010/photos/*"), false}, |
||||
{[]byte(`"mybucket/myobject*"`), Resource{}, true}, |
||||
{[]byte(`"arn:aws:s3:::/*"`), Resource{}, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
var result Resource |
||||
err := json.Unmarshal(testCase.data, &result) |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceValidate(t *testing.T) { |
||||
testCases := []struct { |
||||
resource Resource |
||||
expectErr bool |
||||
}{ |
||||
{NewResource("mybucket", "/myobject*"), false}, |
||||
{NewResource("", "/myobject*"), false}, |
||||
{NewResource("", ""), true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
err := testCase.resource.Validate() |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,147 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"sort" |
||||
|
||||
"github.com/minio/minio-go/pkg/set" |
||||
) |
||||
|
||||
// ResourceSet - set of resources in policy statement.
|
||||
type ResourceSet map[Resource]struct{} |
||||
|
||||
// bucketResourceExists - checks if at least one bucket resource exists in the set.
|
||||
func (resourceSet ResourceSet) bucketResourceExists() bool { |
||||
for resource := range resourceSet { |
||||
if resource.isBucketPattern() { |
||||
return true |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
// objectResourceExists - checks if at least one object resource exists in the set.
|
||||
func (resourceSet ResourceSet) objectResourceExists() bool { |
||||
for resource := range resourceSet { |
||||
if resource.isObjectPattern() { |
||||
return true |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
// Add - adds resource to resource set.
|
||||
func (resourceSet ResourceSet) Add(resource Resource) { |
||||
resourceSet[resource] = struct{}{} |
||||
} |
||||
|
||||
// Intersection - returns resources available in both ResourceSet.
|
||||
func (resourceSet ResourceSet) Intersection(sset ResourceSet) ResourceSet { |
||||
nset := NewResourceSet() |
||||
for k := range resourceSet { |
||||
if _, ok := sset[k]; ok { |
||||
nset.Add(k) |
||||
} |
||||
} |
||||
|
||||
return nset |
||||
} |
||||
|
||||
// MarshalJSON - encodes ResourceSet to JSON data.
|
||||
func (resourceSet ResourceSet) MarshalJSON() ([]byte, error) { |
||||
if len(resourceSet) == 0 { |
||||
return nil, fmt.Errorf("empty resource set") |
||||
} |
||||
|
||||
resources := []Resource{} |
||||
for resource := range resourceSet { |
||||
resources = append(resources, resource) |
||||
} |
||||
|
||||
return json.Marshal(resources) |
||||
} |
||||
|
||||
// Match - matches object name with anyone of resource pattern in resource set.
|
||||
func (resourceSet ResourceSet) Match(resource string) bool { |
||||
for r := range resourceSet { |
||||
if r.Match(resource) { |
||||
return true |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
func (resourceSet ResourceSet) String() string { |
||||
resources := []string{} |
||||
for resource := range resourceSet { |
||||
resources = append(resources, resource.String()) |
||||
} |
||||
sort.Strings(resources) |
||||
|
||||
return fmt.Sprintf("%v", resources) |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data to ResourceSet.
|
||||
func (resourceSet *ResourceSet) UnmarshalJSON(data []byte) error { |
||||
var sset set.StringSet |
||||
if err := json.Unmarshal(data, &sset); err != nil { |
||||
return err |
||||
} |
||||
|
||||
*resourceSet = make(ResourceSet) |
||||
for _, s := range sset.ToSlice() { |
||||
resource, err := parseResource(s) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, found := (*resourceSet)[resource]; found { |
||||
return fmt.Errorf("duplicate resource '%v' found", s) |
||||
} |
||||
|
||||
resourceSet.Add(resource) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Validate - validates ResourceSet.
|
||||
func (resourceSet ResourceSet) Validate() error { |
||||
for resource := range resourceSet { |
||||
if err := resource.Validate(); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// NewResourceSet - creates new resource set.
|
||||
func NewResourceSet(resources ...Resource) ResourceSet { |
||||
resourceSet := make(ResourceSet) |
||||
for _, resource := range resources { |
||||
resourceSet.Add(resource) |
||||
} |
||||
|
||||
return resourceSet |
||||
} |
@ -0,0 +1,239 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func TestResourceSetBucketResourceExists(t *testing.T) { |
||||
testCases := []struct { |
||||
resourceSet ResourceSet |
||||
expectedResult bool |
||||
}{ |
||||
{NewResourceSet(NewResource("*", "")), true}, |
||||
{NewResourceSet(NewResource("mybucket", "")), true}, |
||||
{NewResourceSet(NewResource("mybucket*", "")), true}, |
||||
{NewResourceSet(NewResource("mybucket?0", "")), true}, |
||||
{NewResourceSet(NewResource("mybucket", "/2010/photos/*"), NewResource("mybucket", "")), true}, |
||||
{NewResourceSet(NewResource("", "*")), false}, |
||||
{NewResourceSet(NewResource("*", "*")), false}, |
||||
{NewResourceSet(NewResource("mybucket", "*")), false}, |
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), false}, |
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.resourceSet.bucketResourceExists() |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceSetObjectResourceExists(t *testing.T) { |
||||
testCases := []struct { |
||||
resourceSet ResourceSet |
||||
expectedResult bool |
||||
}{ |
||||
{NewResourceSet(NewResource("*", "")), true}, |
||||
{NewResourceSet(NewResource("mybucket*", "")), true}, |
||||
{NewResourceSet(NewResource("", "*")), true}, |
||||
{NewResourceSet(NewResource("*", "*")), true}, |
||||
{NewResourceSet(NewResource("mybucket", "*")), true}, |
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), true}, |
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), true}, |
||||
{NewResourceSet(NewResource("mybucket", ""), NewResource("mybucket", "/2910/photos/*")), true}, |
||||
{NewResourceSet(NewResource("mybucket", "")), false}, |
||||
{NewResourceSet(NewResource("mybucket?0", "")), false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.resourceSet.objectResourceExists() |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceSetAdd(t *testing.T) { |
||||
testCases := []struct { |
||||
resourceSet ResourceSet |
||||
resource Resource |
||||
expectedResult ResourceSet |
||||
}{ |
||||
{NewResourceSet(), NewResource("mybucket", "/myobject*"), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*"))}, |
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
NewResource("mybucket", "/yourobject*"), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*"), |
||||
NewResource("mybucket", "/yourobject*"))}, |
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
NewResource("mybucket", "/myobject*"), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*"))}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
testCase.resourceSet.Add(testCase.resource) |
||||
|
||||
if !reflect.DeepEqual(testCase.resourceSet, testCase.expectedResult) { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.resourceSet) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceSetIntersection(t *testing.T) { |
||||
testCases := []struct { |
||||
set ResourceSet |
||||
setToIntersect ResourceSet |
||||
expectedResult ResourceSet |
||||
}{ |
||||
{NewResourceSet(), NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet()}, |
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), NewResourceSet(), NewResourceSet()}, |
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*"), NewResource("mybucket", "/yourobject*")), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*"))}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.set.Intersection(testCase.setToIntersect) |
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, testCase.set) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceSetMarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
resoruceSet ResourceSet |
||||
expectedResult []byte |
||||
expectErr bool |
||||
}{ |
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
[]byte(`["arn:aws:s3:::mybucket/myobject*"]`), false}, |
||||
{NewResourceSet(NewResource("mybucket", "/photos/myobject*")), |
||||
[]byte(`["arn:aws:s3:::mybucket/photos/myobject*"]`), false}, |
||||
{NewResourceSet(), nil, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result, err := json.Marshal(testCase.resoruceSet) |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceSetMatch(t *testing.T) { |
||||
testCases := []struct { |
||||
resourceSet ResourceSet |
||||
resource string |
||||
expectedResult bool |
||||
}{ |
||||
{NewResourceSet(NewResource("*", "")), "mybucket", true}, |
||||
{NewResourceSet(NewResource("*", "")), "mybucket/myobject", true}, |
||||
{NewResourceSet(NewResource("mybucket*", "")), "mybucket", true}, |
||||
{NewResourceSet(NewResource("mybucket*", "")), "mybucket/myobject", true}, |
||||
{NewResourceSet(NewResource("", "*")), "/myobject", true}, |
||||
{NewResourceSet(NewResource("*", "*")), "mybucket/myobject", true}, |
||||
{NewResourceSet(NewResource("mybucket", "*")), "mybucket/myobject", true}, |
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket/myobject", true}, |
||||
{NewResourceSet(NewResource("mybucket*", "/myobject")), "mybucket100/myobject", true}, |
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*")), "mybucket20/2010/photos/1.jpg", true}, |
||||
{NewResourceSet(NewResource("mybucket", "")), "mybucket", true}, |
||||
{NewResourceSet(NewResource("mybucket?0", "")), "mybucket30", true}, |
||||
{NewResourceSet(NewResource("mybucket?0", "/2010/photos/*"), |
||||
NewResource("mybucket", "/2010/photos/*")), "mybucket/2010/photos/1.jpg", true}, |
||||
{NewResourceSet(NewResource("", "*")), "mybucket/myobject", false}, |
||||
{NewResourceSet(NewResource("*", "*")), "mybucket", false}, |
||||
{NewResourceSet(NewResource("mybucket", "*")), "mybucket10/myobject", false}, |
||||
{NewResourceSet(NewResource("mybucket", "")), "mybucket/myobject", true}, |
||||
{NewResourceSet(), "mybucket/myobject", false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.resourceSet.Match(testCase.resource) |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceSetUnmarshalJSON(t *testing.T) { |
||||
testCases := []struct { |
||||
data []byte |
||||
expectedResult ResourceSet |
||||
expectErr bool |
||||
}{ |
||||
{[]byte(`"arn:aws:s3:::mybucket/myobject*"`), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), false}, |
||||
{[]byte(`"arn:aws:s3:::mybucket/photos/myobject*"`), |
||||
NewResourceSet(NewResource("mybucket", "/photos/myobject*")), false}, |
||||
{[]byte(`"arn:aws:s3:::mybucket"`), NewResourceSet(NewResource("mybucket", "")), false}, |
||||
{[]byte(`"mybucket/myobject*"`), nil, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
var result ResourceSet |
||||
err := json.Unmarshal(testCase.data, &result) |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestResourceSetValidate(t *testing.T) { |
||||
testCases := []struct { |
||||
resourceSet ResourceSet |
||||
expectErr bool |
||||
}{ |
||||
{NewResourceSet(NewResource("mybucket", "/myobject*")), false}, |
||||
{NewResourceSet(NewResource("", "/myobject*")), false}, |
||||
{NewResourceSet(NewResource("", "")), true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
err := testCase.resourceSet.Validate() |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,141 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"github.com/minio/minio/pkg/policy" |
||||
"github.com/minio/minio/pkg/policy/condition" |
||||
) |
||||
|
||||
// Statement - iam policy statement.
|
||||
type Statement struct { |
||||
SID policy.ID `json:"Sid,omitempty"` |
||||
Effect policy.Effect `json:"Effect"` |
||||
Actions ActionSet `json:"Action"` |
||||
Resources ResourceSet `json:"Resource"` |
||||
Conditions condition.Functions `json:"Condition,omitempty"` |
||||
} |
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
func (statement Statement) IsAllowed(args Args) bool { |
||||
check := func() bool { |
||||
if !statement.Actions.Match(args.Action) { |
||||
return false |
||||
} |
||||
|
||||
resource := args.BucketName |
||||
if args.ObjectName != "" { |
||||
if !strings.HasPrefix(args.ObjectName, "/") { |
||||
resource += "/" |
||||
} |
||||
|
||||
resource += args.ObjectName |
||||
} |
||||
|
||||
if !statement.Resources.Match(resource) { |
||||
return false |
||||
} |
||||
|
||||
return statement.Conditions.Evaluate(args.ConditionValues) |
||||
} |
||||
|
||||
return statement.Effect.IsAllowed(check()) |
||||
} |
||||
|
||||
// isValid - checks whether statement is valid or not.
|
||||
func (statement Statement) isValid() error { |
||||
if !statement.Effect.IsValid() { |
||||
return fmt.Errorf("invalid Effect %v", statement.Effect) |
||||
} |
||||
|
||||
if len(statement.Actions) == 0 { |
||||
return fmt.Errorf("Action must not be empty") |
||||
} |
||||
|
||||
if len(statement.Resources) == 0 { |
||||
return fmt.Errorf("Resource must not be empty") |
||||
} |
||||
|
||||
if err := statement.Resources.Validate(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
for action := range statement.Actions { |
||||
if !statement.Resources.objectResourceExists() && !statement.Resources.bucketResourceExists() { |
||||
return fmt.Errorf("unsupported Resource found %v for action %v", statement.Resources, action) |
||||
} |
||||
|
||||
keys := statement.Conditions.Keys() |
||||
keyDiff := keys.Difference(actionConditionKeyMap[action]) |
||||
if !keyDiff.IsEmpty() { |
||||
return fmt.Errorf("unsupported condition keys '%v' used for action '%v'", keyDiff, action) |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// MarshalJSON - encodes JSON data to Statement.
|
||||
func (statement Statement) MarshalJSON() ([]byte, error) { |
||||
if err := statement.isValid(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// subtype to avoid recursive call to MarshalJSON()
|
||||
type subStatement Statement |
||||
ss := subStatement(statement) |
||||
return json.Marshal(ss) |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data to Statement.
|
||||
func (statement *Statement) UnmarshalJSON(data []byte) error { |
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subStatement Statement |
||||
var ss subStatement |
||||
|
||||
if err := json.Unmarshal(data, &ss); err != nil { |
||||
return err |
||||
} |
||||
|
||||
s := Statement(ss) |
||||
if err := s.isValid(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
*statement = s |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Validate - validates Statement is for given bucket or not.
|
||||
func (statement Statement) Validate() error { |
||||
return statement.isValid() |
||||
} |
||||
|
||||
// NewStatement - creates new statement.
|
||||
func NewStatement(effect policy.Effect, actionSet ActionSet, resourceSet ResourceSet, conditions condition.Functions) Statement { |
||||
return Statement{ |
||||
Effect: effect, |
||||
Actions: actionSet, |
||||
Resources: resourceSet, |
||||
Conditions: conditions, |
||||
} |
||||
} |
@ -0,0 +1,515 @@ |
||||
/* |
||||
* 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 iampolicy |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net" |
||||
"reflect" |
||||
"testing" |
||||
|
||||
"github.com/minio/minio/pkg/policy" |
||||
"github.com/minio/minio/pkg/policy/condition" |
||||
) |
||||
|
||||
func TestStatementIsAllowed(t *testing.T) { |
||||
case1Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetBucketLocationAction, PutObjectAction), |
||||
NewResourceSet(NewResource("*", "")), |
||||
condition.NewFunctions(), |
||||
) |
||||
|
||||
case2Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetObjectAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(), |
||||
) |
||||
|
||||
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24") |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
func1, err := condition.NewIPAddressFunc( |
||||
condition.AWSSourceIP, |
||||
IPNet1, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
|
||||
case3Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetObjectAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(func1), |
||||
) |
||||
|
||||
case4Statement := NewStatement( |
||||
policy.Deny, |
||||
NewActionSet(GetObjectAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(func1), |
||||
) |
||||
|
||||
anonGetBucketLocationArgs := Args{ |
||||
AccountName: "Q3AM3UQ867SPQQA43P2F", |
||||
Action: GetBucketLocationAction, |
||||
BucketName: "mybucket", |
||||
ConditionValues: map[string][]string{}, |
||||
} |
||||
|
||||
anonPutObjectActionArgs := Args{ |
||||
AccountName: "Q3AM3UQ867SPQQA43P2F", |
||||
Action: PutObjectAction, |
||||
BucketName: "mybucket", |
||||
ConditionValues: map[string][]string{ |
||||
"x-amz-copy-source": {"mybucket/myobject"}, |
||||
"SourceIp": {"192.168.1.10"}, |
||||
}, |
||||
ObjectName: "myobject", |
||||
} |
||||
|
||||
anonGetObjectActionArgs := Args{ |
||||
AccountName: "Q3AM3UQ867SPQQA43P2F", |
||||
Action: GetObjectAction, |
||||
BucketName: "mybucket", |
||||
ConditionValues: map[string][]string{}, |
||||
ObjectName: "myobject", |
||||
} |
||||
|
||||
getBucketLocationArgs := Args{ |
||||
AccountName: "Q3AM3UQ867SPQQA43P2F", |
||||
Action: GetBucketLocationAction, |
||||
BucketName: "mybucket", |
||||
ConditionValues: map[string][]string{}, |
||||
} |
||||
|
||||
putObjectActionArgs := Args{ |
||||
AccountName: "Q3AM3UQ867SPQQA43P2F", |
||||
Action: PutObjectAction, |
||||
BucketName: "mybucket", |
||||
ConditionValues: map[string][]string{ |
||||
"x-amz-copy-source": {"mybucket/myobject"}, |
||||
"SourceIp": {"192.168.1.10"}, |
||||
}, |
||||
ObjectName: "myobject", |
||||
} |
||||
|
||||
getObjectActionArgs := Args{ |
||||
AccountName: "Q3AM3UQ867SPQQA43P2F", |
||||
Action: GetObjectAction, |
||||
BucketName: "mybucket", |
||||
ConditionValues: map[string][]string{}, |
||||
ObjectName: "myobject", |
||||
} |
||||
|
||||
testCases := []struct { |
||||
statement Statement |
||||
args Args |
||||
expectedResult bool |
||||
}{ |
||||
{case1Statement, anonGetBucketLocationArgs, true}, |
||||
{case1Statement, anonPutObjectActionArgs, true}, |
||||
{case1Statement, anonGetObjectActionArgs, false}, |
||||
{case1Statement, getBucketLocationArgs, true}, |
||||
{case1Statement, putObjectActionArgs, true}, |
||||
{case1Statement, getObjectActionArgs, false}, |
||||
|
||||
{case2Statement, anonGetBucketLocationArgs, false}, |
||||
{case2Statement, anonPutObjectActionArgs, true}, |
||||
{case2Statement, anonGetObjectActionArgs, true}, |
||||
{case2Statement, getBucketLocationArgs, false}, |
||||
{case2Statement, putObjectActionArgs, true}, |
||||
{case2Statement, getObjectActionArgs, true}, |
||||
|
||||
{case3Statement, anonGetBucketLocationArgs, false}, |
||||
{case3Statement, anonPutObjectActionArgs, true}, |
||||
{case3Statement, anonGetObjectActionArgs, false}, |
||||
{case3Statement, getBucketLocationArgs, false}, |
||||
{case3Statement, putObjectActionArgs, true}, |
||||
{case3Statement, getObjectActionArgs, false}, |
||||
|
||||
{case4Statement, anonGetBucketLocationArgs, true}, |
||||
{case4Statement, anonPutObjectActionArgs, false}, |
||||
{case4Statement, anonGetObjectActionArgs, true}, |
||||
{case4Statement, getBucketLocationArgs, true}, |
||||
{case4Statement, putObjectActionArgs, false}, |
||||
{case4Statement, getObjectActionArgs, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result := testCase.statement.IsAllowed(testCase.args) |
||||
|
||||
if result != testCase.expectedResult { |
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestStatementIsValid(t *testing.T) { |
||||
_, IPNet1, err := net.ParseCIDR("192.168.1.0/24") |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
func1, err := condition.NewIPAddressFunc( |
||||
condition.AWSSourceIP, |
||||
IPNet1, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
|
||||
func2, err := condition.NewStringEqualsFunc( |
||||
condition.S3XAmzCopySource, |
||||
"mybucket/myobject", |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
|
||||
testCases := []struct { |
||||
statement Statement |
||||
expectErr bool |
||||
}{ |
||||
// Invalid effect error.
|
||||
{NewStatement( |
||||
policy.Effect("foo"), |
||||
NewActionSet(GetBucketLocationAction, PutObjectAction), |
||||
NewResourceSet(NewResource("*", "")), |
||||
condition.NewFunctions(), |
||||
), true}, |
||||
// Empty actions error.
|
||||
{NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(), |
||||
NewResourceSet(NewResource("*", "")), |
||||
condition.NewFunctions(), |
||||
), true}, |
||||
// Empty resources error.
|
||||
{NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetBucketLocationAction, PutObjectAction), |
||||
NewResourceSet(), |
||||
condition.NewFunctions(), |
||||
), true}, |
||||
// Unsupported conditions for GetObject
|
||||
{NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetObjectAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "myobject*")), |
||||
condition.NewFunctions(func1, func2), |
||||
), true}, |
||||
{NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetBucketLocationAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "myobject*")), |
||||
condition.NewFunctions(), |
||||
), false}, |
||||
{NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetBucketLocationAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "")), |
||||
condition.NewFunctions(), |
||||
), false}, |
||||
{NewStatement( |
||||
policy.Deny, |
||||
NewActionSet(GetObjectAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "myobject*")), |
||||
condition.NewFunctions(func1), |
||||
), false}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
err := testCase.statement.isValid() |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestStatementMarshalJSON(t *testing.T) { |
||||
case1Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(), |
||||
) |
||||
case1Statement.SID = "SomeId1" |
||||
case1Data := []byte(`{"Sid":"SomeId1","Effect":"Allow","Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"]}`) |
||||
|
||||
func1, err := condition.NewNullFunc( |
||||
condition.S3XAmzCopySource, |
||||
true, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
case2Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(func1), |
||||
) |
||||
case2Data := []byte(`{"Effect":"Allow","Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-copy-source":[true]}}}`) |
||||
|
||||
func2, err := condition.NewNullFunc( |
||||
condition.S3XAmzServerSideEncryption, |
||||
false, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
case3Statement := NewStatement( |
||||
policy.Deny, |
||||
NewActionSet(GetObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(func2), |
||||
) |
||||
case3Data := []byte(`{"Effect":"Deny","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-server-side-encryption":[false]}}}`) |
||||
|
||||
case4Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetObjectAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "myobject*")), |
||||
condition.NewFunctions(func1, func2), |
||||
) |
||||
|
||||
testCases := []struct { |
||||
statement Statement |
||||
expectedResult []byte |
||||
expectErr bool |
||||
}{ |
||||
{case1Statement, case1Data, false}, |
||||
{case2Statement, case2Data, false}, |
||||
{case3Statement, case3Data, false}, |
||||
// Invalid statement error.
|
||||
{case4Statement, nil, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
result, err := json.Marshal(testCase.statement) |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestStatementUnmarshalJSON(t *testing.T) { |
||||
case1Data := []byte(`{ |
||||
"Sid": "SomeId1", |
||||
"Effect": "Allow", |
||||
"Action": "s3:PutObject", |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*" |
||||
}`) |
||||
case1Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(), |
||||
) |
||||
case1Statement.SID = "SomeId1" |
||||
|
||||
case2Data := []byte(`{ |
||||
"Effect": "Allow", |
||||
"Action": "s3:PutObject", |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*", |
||||
"Condition": { |
||||
"Null": { |
||||
"s3:x-amz-copy-source": true |
||||
} |
||||
} |
||||
}`) |
||||
func1, err := condition.NewNullFunc( |
||||
condition.S3XAmzCopySource, |
||||
true, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
case2Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(func1), |
||||
) |
||||
|
||||
case3Data := []byte(`{ |
||||
"Effect": "Deny", |
||||
"Action": [ |
||||
"s3:PutObject", |
||||
"s3:GetObject" |
||||
], |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*", |
||||
"Condition": { |
||||
"Null": { |
||||
"s3:x-amz-server-side-encryption": "false" |
||||
} |
||||
} |
||||
}`) |
||||
func2, err := condition.NewNullFunc( |
||||
condition.S3XAmzServerSideEncryption, |
||||
false, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
case3Statement := NewStatement( |
||||
policy.Deny, |
||||
NewActionSet(PutObjectAction, GetObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(func2), |
||||
) |
||||
|
||||
case4Data := []byte(`{ |
||||
"Effect": "Allow", |
||||
"Action": "s3:PutObjec", |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*" |
||||
}`) |
||||
|
||||
case5Data := []byte(`{ |
||||
"Action": "s3:PutObject", |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*" |
||||
}`) |
||||
|
||||
case7Data := []byte(`{ |
||||
"Effect": "Allow", |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*" |
||||
}`) |
||||
|
||||
case8Data := []byte(`{ |
||||
"Effect": "Allow", |
||||
"Action": "s3:PutObject" |
||||
}`) |
||||
|
||||
case9Data := []byte(`{ |
||||
"Effect": "Allow", |
||||
"Action": "s3:PutObject", |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*", |
||||
"Condition": { |
||||
} |
||||
}`) |
||||
|
||||
case10Data := []byte(`{ |
||||
"Effect": "Deny", |
||||
"Action": [ |
||||
"s3:PutObject", |
||||
"s3:GetObject" |
||||
], |
||||
"Resource": "arn:aws:s3:::mybucket/myobject*", |
||||
"Condition": { |
||||
"StringEquals": { |
||||
"s3:x-amz-copy-source": "yourbucket/myobject*" |
||||
} |
||||
} |
||||
}`) |
||||
|
||||
testCases := []struct { |
||||
data []byte |
||||
expectedResult Statement |
||||
expectErr bool |
||||
}{ |
||||
{case1Data, case1Statement, false}, |
||||
{case2Data, case2Statement, false}, |
||||
{case3Data, case3Statement, false}, |
||||
// JSON unmarshaling error.
|
||||
{case4Data, Statement{}, true}, |
||||
// Invalid effect error.
|
||||
{case5Data, Statement{}, true}, |
||||
// Empty action error.
|
||||
{case7Data, Statement{}, true}, |
||||
// Empty resource error.
|
||||
{case8Data, Statement{}, true}, |
||||
// Empty condition error.
|
||||
{case9Data, Statement{}, true}, |
||||
// Unsupported condition key error.
|
||||
{case10Data, Statement{}, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
var result Statement |
||||
expectErr := (json.Unmarshal(testCase.data, &result) != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
|
||||
if !testCase.expectErr { |
||||
if !reflect.DeepEqual(result, testCase.expectedResult) { |
||||
t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestStatementValidate(t *testing.T) { |
||||
case1Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "/myobject*")), |
||||
condition.NewFunctions(), |
||||
) |
||||
|
||||
func1, err := condition.NewNullFunc( |
||||
condition.S3XAmzCopySource, |
||||
true, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
func2, err := condition.NewNullFunc( |
||||
condition.S3XAmzServerSideEncryption, |
||||
false, |
||||
) |
||||
if err != nil { |
||||
t.Fatalf("unexpected error. %v\n", err) |
||||
} |
||||
case2Statement := NewStatement( |
||||
policy.Allow, |
||||
NewActionSet(GetObjectAction, PutObjectAction), |
||||
NewResourceSet(NewResource("mybucket", "myobject*")), |
||||
condition.NewFunctions(func1, func2), |
||||
) |
||||
|
||||
testCases := []struct { |
||||
statement Statement |
||||
expectErr bool |
||||
}{ |
||||
{case1Statement, false}, |
||||
{case2Statement, true}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
err := testCase.statement.Validate() |
||||
expectErr := (err != nil) |
||||
|
||||
if expectErr != testCase.expectErr { |
||||
t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,137 @@ |
||||
/* |
||||
* 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 validator |
||||
|
||||
import ( |
||||
"crypto" |
||||
"crypto/ecdsa" |
||||
"crypto/elliptic" |
||||
"crypto/rsa" |
||||
"encoding/base64" |
||||
"encoding/binary" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
"strings" |
||||
) |
||||
|
||||
// JWKS - https://tools.ietf.org/html/rfc7517
|
||||
type JWKS struct { |
||||
Keys []*JWKS `json:"keys,omitempty"` |
||||
|
||||
Kty string `json:"kty"` |
||||
Use string `json:"use,omitempty"` |
||||
Kid string `json:"kid,omitempty"` |
||||
Alg string `json:"alg,omitempty"` |
||||
|
||||
Crv string `json:"crv,omitempty"` |
||||
X string `json:"x,omitempty"` |
||||
Y string `json:"y,omitempty"` |
||||
D string `json:"d,omitempty"` |
||||
N string `json:"n,omitempty"` |
||||
E string `json:"e,omitempty"` |
||||
K string `json:"k,omitempty"` |
||||
} |
||||
|
||||
func safeDecode(str string) ([]byte, error) { |
||||
lenMod4 := len(str) % 4 |
||||
if lenMod4 > 0 { |
||||
str = str + strings.Repeat("=", 4-lenMod4) |
||||
} |
||||
|
||||
return base64.URLEncoding.DecodeString(str) |
||||
} |
||||
|
||||
var ( |
||||
errMalformedJWKRSAKey = errors.New("malformed JWK RSA key") |
||||
errMalformedJWKECKey = errors.New("malformed JWK EC key") |
||||
) |
||||
|
||||
// DecodePublicKey - decodes JSON Web Key (JWK) as public key
|
||||
func (key *JWKS) DecodePublicKey() (crypto.PublicKey, error) { |
||||
switch key.Kty { |
||||
case "RSA": |
||||
if key.N == "" || key.E == "" { |
||||
return nil, errMalformedJWKRSAKey |
||||
} |
||||
|
||||
// decode exponent
|
||||
data, err := safeDecode(key.E) |
||||
if err != nil { |
||||
return nil, errMalformedJWKRSAKey |
||||
} |
||||
|
||||
if len(data) < 4 { |
||||
ndata := make([]byte, 4) |
||||
copy(ndata[4-len(data):], data) |
||||
data = ndata |
||||
} |
||||
|
||||
pubKey := &rsa.PublicKey{ |
||||
N: &big.Int{}, |
||||
E: int(binary.BigEndian.Uint32(data[:])), |
||||
} |
||||
|
||||
data, err = safeDecode(key.N) |
||||
if err != nil { |
||||
return nil, errMalformedJWKRSAKey |
||||
} |
||||
pubKey.N.SetBytes(data) |
||||
|
||||
return pubKey, nil |
||||
case "EC": |
||||
if key.Crv == "" || key.X == "" || key.Y == "" { |
||||
return nil, errMalformedJWKECKey |
||||
} |
||||
|
||||
var curve elliptic.Curve |
||||
switch key.Crv { |
||||
case "P-224": |
||||
curve = elliptic.P224() |
||||
case "P-256": |
||||
curve = elliptic.P256() |
||||
case "P-384": |
||||
curve = elliptic.P384() |
||||
case "P-521": |
||||
curve = elliptic.P521() |
||||
default: |
||||
return nil, fmt.Errorf("Unknown curve type: %s", key.Crv) |
||||
} |
||||
|
||||
pubKey := &ecdsa.PublicKey{ |
||||
Curve: curve, |
||||
X: &big.Int{}, |
||||
Y: &big.Int{}, |
||||
} |
||||
|
||||
data, err := safeDecode(key.X) |
||||
if err != nil { |
||||
return nil, errMalformedJWKECKey |
||||
} |
||||
pubKey.X.SetBytes(data) |
||||
|
||||
data, err = safeDecode(key.Y) |
||||
if err != nil { |
||||
return nil, errMalformedJWKECKey |
||||
} |
||||
pubKey.Y.SetBytes(data) |
||||
|
||||
return pubKey, nil |
||||
default: |
||||
return nil, fmt.Errorf("Unknown JWK key type %s", key.Kty) |
||||
} |
||||
} |
@ -0,0 +1,103 @@ |
||||
/* |
||||
* 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 validator |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto" |
||||
"crypto/ecdsa" |
||||
"crypto/elliptic" |
||||
"crypto/rsa" |
||||
"encoding/json" |
||||
"testing" |
||||
) |
||||
|
||||
// A.1 - Example public keys
|
||||
func TestPublicKey(t *testing.T) { |
||||
const jsonkey = `{"keys": |
||||
[ |
||||
{"kty":"EC", |
||||
"crv":"P-256", |
||||
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", |
||||
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", |
||||
"use":"enc", |
||||
"kid":"1"}, |
||||
|
||||
{"kty":"RSA", |
||||
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", |
||||
"e":"AQAB", |
||||
"alg":"RS256", |
||||
"kid":"2011-04-29"} |
||||
] |
||||
}` |
||||
|
||||
var jk JWKS |
||||
if err := json.Unmarshal([]byte(jsonkey), &jk); err != nil { |
||||
t.Fatal("Unmarshal: ", err) |
||||
} else if len(jk.Keys) != 2 { |
||||
t.Fatalf("Expected 2 keys, got %d", len(jk.Keys)) |
||||
} |
||||
|
||||
keys := make([]crypto.PublicKey, len(jk.Keys)) |
||||
for ii, jks := range jk.Keys { |
||||
var err error |
||||
keys[ii], err = jks.DecodePublicKey() |
||||
if err != nil { |
||||
t.Fatalf("Failed to decode key %d: %v", ii, err) |
||||
} |
||||
} |
||||
|
||||
if key0, ok := keys[0].(*ecdsa.PublicKey); !ok { |
||||
t.Fatalf("Expected ECDSA key[0], got %T", keys[0]) |
||||
} else if key1, ok := keys[1].(*rsa.PublicKey); !ok { |
||||
t.Fatalf("Expected RSA key[1], got %T", keys[1]) |
||||
} else if key0.Curve != elliptic.P256() { |
||||
t.Fatal("Key[0] is not using P-256 curve") |
||||
} else if !bytes.Equal(key0.X.Bytes(), []byte{0x30, 0xa0, 0x42, 0x4c, 0xd2, |
||||
0x1c, 0x29, 0x44, 0x83, 0x8a, 0x2d, 0x75, 0xc9, 0x2b, 0x37, 0xe7, 0x6e, 0xa2, |
||||
0xd, 0x9f, 0x0, 0x89, 0x3a, 0x3b, 0x4e, 0xee, 0x8a, 0x3c, 0xa, 0xaf, 0xec, 0x3e}) { |
||||
t.Fatalf("Bad key[0].X, got %v", key0.X.Bytes()) |
||||
} else if !bytes.Equal(key0.Y.Bytes(), []byte{0xe0, 0x4b, 0x65, 0xe9, 0x24, |
||||
0x56, 0xd9, 0x88, 0x8b, 0x52, 0xb3, 0x79, 0xbd, 0xfb, 0xd5, 0x1e, 0xe8, |
||||
0x69, 0xef, 0x1f, 0xf, 0xc6, 0x5b, 0x66, 0x59, 0x69, 0x5b, 0x6c, 0xce, |
||||
0x8, 0x17, 0x23}) { |
||||
t.Fatalf("Bad key[0].Y, got %v", key0.Y.Bytes()) |
||||
} else if key1.E != 0x10001 { |
||||
t.Fatalf("Bad key[1].E: %d", key1.E) |
||||
} else if !bytes.Equal(key1.N.Bytes(), []byte{0xd2, 0xfc, 0x7b, 0x6a, 0xa, 0x1e, |
||||
0x6c, 0x67, 0x10, 0x4a, 0xeb, 0x8f, 0x88, 0xb2, 0x57, 0x66, 0x9b, 0x4d, 0xf6, |
||||
0x79, 0xdd, 0xad, 0x9, 0x9b, 0x5c, 0x4a, 0x6c, 0xd9, 0xa8, 0x80, 0x15, 0xb5, |
||||
0xa1, 0x33, 0xbf, 0xb, 0x85, 0x6c, 0x78, 0x71, 0xb6, 0xdf, 0x0, 0xb, 0x55, |
||||
0x4f, 0xce, 0xb3, 0xc2, 0xed, 0x51, 0x2b, 0xb6, 0x8f, 0x14, 0x5c, 0x6e, 0x84, |
||||
0x34, 0x75, 0x2f, 0xab, 0x52, 0xa1, 0xcf, 0xc1, 0x24, 0x40, 0x8f, 0x79, 0xb5, |
||||
0x8a, 0x45, 0x78, 0xc1, 0x64, 0x28, 0x85, 0x57, 0x89, 0xf7, 0xa2, 0x49, 0xe3, |
||||
0x84, 0xcb, 0x2d, 0x9f, 0xae, 0x2d, 0x67, 0xfd, 0x96, 0xfb, 0x92, 0x6c, 0x19, |
||||
0x8e, 0x7, 0x73, 0x99, 0xfd, 0xc8, 0x15, 0xc0, 0xaf, 0x9, 0x7d, 0xde, 0x5a, |
||||
0xad, 0xef, 0xf4, 0x4d, 0xe7, 0xe, 0x82, 0x7f, 0x48, 0x78, 0x43, 0x24, 0x39, |
||||
0xbf, 0xee, 0xb9, 0x60, 0x68, 0xd0, 0x47, 0x4f, 0xc5, 0xd, 0x6d, 0x90, 0xbf, |
||||
0x3a, 0x98, 0xdf, 0xaf, 0x10, 0x40, 0xc8, 0x9c, 0x2, 0xd6, 0x92, 0xab, 0x3b, |
||||
0x3c, 0x28, 0x96, 0x60, 0x9d, 0x86, 0xfd, 0x73, 0xb7, 0x74, 0xce, 0x7, 0x40, |
||||
0x64, 0x7c, 0xee, 0xea, 0xa3, 0x10, 0xbd, 0x12, 0xf9, 0x85, 0xa8, 0xeb, 0x9f, |
||||
0x59, 0xfd, 0xd4, 0x26, 0xce, 0xa5, 0xb2, 0x12, 0xf, 0x4f, 0x2a, 0x34, 0xbc, |
||||
0xab, 0x76, 0x4b, 0x7e, 0x6c, 0x54, 0xd6, 0x84, 0x2, 0x38, 0xbc, 0xc4, 0x5, 0x87, |
||||
0xa5, 0x9e, 0x66, 0xed, 0x1f, 0x33, 0x89, 0x45, 0x77, 0x63, 0x5c, 0x47, 0xa, |
||||
0xf7, 0x5c, 0xf9, 0x2c, 0x20, 0xd1, 0xda, 0x43, 0xe1, 0xbf, 0xc4, 0x19, 0xe2, |
||||
0x22, 0xa6, 0xf0, 0xd0, 0xbb, 0x35, 0x8c, 0x5e, 0x38, 0xf9, 0xcb, 0x5, 0xa, 0xea, |
||||
0xfe, 0x90, 0x48, 0x14, 0xf1, 0xac, 0x1a, 0xa4, 0x9c, 0xca, 0x9e, 0xa0, 0xca, 0x83}) { |
||||
t.Fatalf("Bad key[1].N, got %v", key1.N.Bytes()) |
||||
} |
||||
} |
@ -0,0 +1,228 @@ |
||||
/* |
||||
* 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 validator |
||||
|
||||
import ( |
||||
"crypto" |
||||
"crypto/tls" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"net" |
||||
"net/http" |
||||
"os" |
||||
"strconv" |
||||
"time" |
||||
|
||||
jwtgo "github.com/dgrijalva/jwt-go" |
||||
xnet "github.com/minio/minio/pkg/net" |
||||
) |
||||
|
||||
// JWKSArgs - RSA authentication target arguments
|
||||
type JWKSArgs struct { |
||||
URL *xnet.URL `json:"url"` |
||||
publicKey crypto.PublicKey |
||||
} |
||||
|
||||
// Validate JWT authentication target arguments
|
||||
func (r *JWKSArgs) Validate() error { |
||||
return nil |
||||
} |
||||
|
||||
// PopulatePublicKey - populates a new publickey from the JWKS URL.
|
||||
func (r *JWKSArgs) PopulatePublicKey() error { |
||||
insecureClient := &http.Client{Transport: newCustomHTTPTransport(true)} |
||||
client := &http.Client{Transport: newCustomHTTPTransport(false)} |
||||
resp, err := client.Get(r.URL.String()) |
||||
if err != nil { |
||||
resp, err = insecureClient.Get(r.URL.String()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
defer resp.Body.Close() |
||||
if resp.StatusCode != http.StatusOK { |
||||
return errors.New(resp.Status) |
||||
} |
||||
|
||||
var jwk JWKS |
||||
if err = json.NewDecoder(resp.Body).Decode(&jwk); err != nil { |
||||
return err |
||||
} |
||||
|
||||
r.publicKey, err = jwk.Keys[0].DecodePublicKey() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// UnmarshalJSON - decodes JSON data.
|
||||
func (r *JWKSArgs) UnmarshalJSON(data []byte) error { |
||||
// subtype to avoid recursive call to UnmarshalJSON()
|
||||
type subJWKSArgs JWKSArgs |
||||
var sr subJWKSArgs |
||||
|
||||
// IAM related envs.
|
||||
if jwksURL, ok := os.LookupEnv("MINIO_IAM_JWKS_URL"); ok { |
||||
u, err := xnet.ParseURL(jwksURL) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
sr.URL = u |
||||
} else { |
||||
if err := json.Unmarshal(data, &sr); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
ar := JWKSArgs(sr) |
||||
if ar.URL == nil || ar.URL.String() == "" { |
||||
*r = ar |
||||
return nil |
||||
} |
||||
if err := ar.Validate(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := ar.PopulatePublicKey(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
*r = ar |
||||
return nil |
||||
} |
||||
|
||||
// JWT - rs client grants provider details.
|
||||
type JWT struct { |
||||
args JWKSArgs |
||||
} |
||||
|
||||
func expToInt64(expI interface{}) (expAt int64, err error) { |
||||
switch exp := expI.(type) { |
||||
case float64: |
||||
expAt = int64(exp) |
||||
case int64: |
||||
expAt = exp |
||||
case json.Number: |
||||
expAt, err = exp.Int64() |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
default: |
||||
return 0, errors.New("invalid expiry value") |
||||
} |
||||
return expAt, nil |
||||
} |
||||
|
||||
func getDefaultExpiration(dsecs string) (time.Duration, error) { |
||||
defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
|
||||
if dsecs != "" { |
||||
expirySecs, err := strconv.ParseInt(dsecs, 10, 64) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
// The duration, in seconds, of the role session.
|
||||
// The value can range from 900 seconds (15 minutes)
|
||||
// to 12 hours.
|
||||
if expirySecs < 900 || expirySecs > 43200 { |
||||
return 0, errors.New("out of range value for duration in seconds") |
||||
} |
||||
|
||||
defaultExpiryDuration = time.Duration(expirySecs) * time.Second |
||||
} |
||||
return defaultExpiryDuration, nil |
||||
} |
||||
|
||||
// newCustomHTTPTransport returns a new http configuration
|
||||
// used while communicating with the cloud backends.
|
||||
// This sets the value for MaxIdleConnsPerHost from 2 (go default)
|
||||
// to 100.
|
||||
func newCustomHTTPTransport(insecure bool) *http.Transport { |
||||
return &http.Transport{ |
||||
Proxy: http.ProxyFromEnvironment, |
||||
DialContext: (&net.Dialer{ |
||||
Timeout: 30 * time.Second, |
||||
KeepAlive: 30 * time.Second, |
||||
}).DialContext, |
||||
MaxIdleConns: 1024, |
||||
MaxIdleConnsPerHost: 1024, |
||||
IdleConnTimeout: 30 * time.Second, |
||||
TLSHandshakeTimeout: 10 * time.Second, |
||||
ExpectContinueTimeout: 1 * time.Second, |
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, |
||||
DisableCompression: true, |
||||
} |
||||
} |
||||
|
||||
// Validate - validates the access token.
|
||||
func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) { |
||||
keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) { |
||||
if _, ok := jwtToken.Method.(*jwtgo.SigningMethodRSA); !ok { |
||||
if _, ok = jwtToken.Method.(*jwtgo.SigningMethodECDSA); ok { |
||||
return p.args.publicKey, nil |
||||
} |
||||
return nil, fmt.Errorf("Unexpected signing method: %v", jwtToken.Header["alg"]) |
||||
} |
||||
return p.args.publicKey, nil |
||||
} |
||||
|
||||
var claims jwtgo.MapClaims |
||||
jwtToken, err := jwtgo.ParseWithClaims(token, &claims, keyFuncCallback) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if !jwtToken.Valid { |
||||
return nil, fmt.Errorf("Invalid token: %v", token) |
||||
} |
||||
|
||||
expAt, err := expToInt64(claims["exp"]) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
defaultExpiryDuration, err := getDefaultExpiration(dsecs) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if time.Unix(expAt, 0).UTC().Sub(time.Now().UTC()) < defaultExpiryDuration { |
||||
defaultExpiryDuration = time.Unix(expAt, 0).UTC().Sub(time.Now().UTC()) |
||||
} |
||||
|
||||
expiry := time.Now().UTC().Add(defaultExpiryDuration).Unix() |
||||
if expAt < expiry { |
||||
claims["exp"] = strconv.FormatInt(expAt, 64) |
||||
} |
||||
|
||||
return claims, nil |
||||
|
||||
} |
||||
|
||||
// ID returns the provider name and authentication type.
|
||||
func (p *JWT) ID() ID { |
||||
return "jwt" |
||||
} |
||||
|
||||
// NewJWT - initialize new jwt authenticator.
|
||||
func NewJWT(args JWKSArgs) *JWT { |
||||
return &JWT{ |
||||
args: args, |
||||
} |
||||
} |
@ -0,0 +1,120 @@ |
||||
/* |
||||
* 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 validator |
||||
|
||||
import ( |
||||
"crypto" |
||||
"encoding/json" |
||||
"net/url" |
||||
"testing" |
||||
"time" |
||||
|
||||
xnet "github.com/minio/minio/pkg/net" |
||||
) |
||||
|
||||
func TestJWT(t *testing.T) { |
||||
const jsonkey = `{"keys": |
||||
[ |
||||
{"kty":"RSA", |
||||
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", |
||||
"e":"AQAB", |
||||
"alg":"RS256", |
||||
"kid":"2011-04-29"} |
||||
] |
||||
}` |
||||
|
||||
var jk JWKS |
||||
if err := json.Unmarshal([]byte(jsonkey), &jk); err != nil { |
||||
t.Fatal("Unmarshal: ", err) |
||||
} else if len(jk.Keys) != 1 { |
||||
t.Fatalf("Expected 1 keys, got %d", len(jk.Keys)) |
||||
} |
||||
|
||||
keys := make([]crypto.PublicKey, len(jk.Keys)) |
||||
for ii, jks := range jk.Keys { |
||||
var err error |
||||
keys[ii], err = jks.DecodePublicKey() |
||||
if err != nil { |
||||
t.Fatalf("Failed to decode key %d: %v", ii, err) |
||||
} |
||||
} |
||||
|
||||
u1, err := xnet.ParseURL("http://localhost:8443") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
jwt := NewJWT(JWKSArgs{ |
||||
URL: u1, |
||||
publicKey: keys[0], |
||||
}) |
||||
if jwt.ID() != "jwt" { |
||||
t.Fatalf("Uexpected id %s for the validator", jwt.ID()) |
||||
} |
||||
|
||||
u, err := url.Parse("http://localhost:8443/?Token=invalid") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if _, err := jwt.Validate(u.Query().Get("Token"), ""); err == nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func TestDefaultExpiryDuration(t *testing.T) { |
||||
testCases := []struct { |
||||
reqURL string |
||||
duration time.Duration |
||||
expectErr bool |
||||
}{ |
||||
{ |
||||
reqURL: "http://localhost:8443/?Token=xxxxx", |
||||
duration: time.Duration(60) * time.Minute, |
||||
}, |
||||
{ |
||||
reqURL: "http://localhost:8443/?DurationSeconds=9s", |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
reqURL: "http://localhost:8443/?DurationSeconds=43201", |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
reqURL: "http://localhost:8443/?DurationSeconds=800", |
||||
expectErr: true, |
||||
}, |
||||
{ |
||||
reqURL: "http://localhost:8443/?DurationSeconds=901", |
||||
duration: time.Duration(901) * time.Second, |
||||
}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
u, err := url.Parse(testCase.reqURL) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
d, err := getDefaultExpiration(u.Query().Get("DurationSeconds")) |
||||
gotErr := (err != nil) |
||||
if testCase.expectErr != gotErr { |
||||
t.Errorf("Test %d: Expected %v, got %v with error %s", i+1, testCase.expectErr, gotErr, err) |
||||
} |
||||
if d != testCase.duration { |
||||
t.Errorf("Test %d: Expected duration %d, got %d", i+1, testCase.duration, d) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,92 @@ |
||||
/* |
||||
* 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 validator |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"sync" |
||||
) |
||||
|
||||
// ID - holds identification name authentication validator target.
|
||||
type ID string |
||||
|
||||
// Validator interface describes basic implementation
|
||||
// requirements of various authentication providers.
|
||||
type Validator interface { |
||||
// Validate is a custom validator function for this provider,
|
||||
// each validation is authenticationType or provider specific.
|
||||
Validate(token string, duration string) (map[string]interface{}, error) |
||||
|
||||
// ID returns provider name of this provider.
|
||||
ID() ID |
||||
} |
||||
|
||||
// ErrTokenExpired - error token expired
|
||||
var ( |
||||
ErrTokenExpired = errors.New("token expired") |
||||
ErrInvalidDuration = errors.New("duration higher than token expiry") |
||||
) |
||||
|
||||
// Validators - holds list of providers indexed by provider id.
|
||||
type Validators struct { |
||||
sync.RWMutex |
||||
providers map[ID]Validator |
||||
} |
||||
|
||||
// Add - adds unique provider to provider list.
|
||||
func (list *Validators) Add(provider Validator) error { |
||||
list.Lock() |
||||
defer list.Unlock() |
||||
|
||||
if _, ok := list.providers[provider.ID()]; ok { |
||||
return fmt.Errorf("provider %v already exists", provider.ID()) |
||||
} |
||||
|
||||
list.providers[provider.ID()] = provider |
||||
return nil |
||||
} |
||||
|
||||
// List - returns available provider IDs.
|
||||
func (list *Validators) List() []ID { |
||||
list.RLock() |
||||
defer list.RUnlock() |
||||
|
||||
keys := []ID{} |
||||
for k := range list.providers { |
||||
keys = append(keys, k) |
||||
} |
||||
|
||||
return keys |
||||
} |
||||
|
||||
// Get - returns the provider for the given providerID, if not found
|
||||
// returns an error.
|
||||
func (list *Validators) Get(id ID) (p Validator, err error) { |
||||
list.RLock() |
||||
defer list.RUnlock() |
||||
var ok bool |
||||
if p, ok = list.providers[id]; !ok { |
||||
return nil, fmt.Errorf("provider %v doesn't exist", id) |
||||
} |
||||
return p, nil |
||||
} |
||||
|
||||
// NewValidators - creates Validators.
|
||||
func NewValidators() *Validators { |
||||
return &Validators{providers: make(map[ID]Validator)} |
||||
} |
@ -0,0 +1,64 @@ |
||||
/* |
||||
* 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 validator |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
type errorValidator struct{} |
||||
|
||||
func (e errorValidator) Validate(token, dsecs string) (map[string]interface{}, error) { |
||||
return nil, ErrTokenExpired |
||||
} |
||||
|
||||
func (e errorValidator) ID() ID { |
||||
return "err" |
||||
} |
||||
|
||||
func TestValidators(t *testing.T) { |
||||
vrs := NewValidators() |
||||
if err := vrs.Add(&errorValidator{}); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if err := vrs.Add(&errorValidator{}); err == nil { |
||||
t.Fatal("Unexpected should return error for double inserts") |
||||
} |
||||
|
||||
if _, err := vrs.Get("unknown"); err == nil { |
||||
t.Fatal("Unexpected should return error for unknown validators") |
||||
} |
||||
|
||||
v, err := vrs.Get("err") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if _, err = v.Validate("", ""); err != ErrTokenExpired { |
||||
t.Fatalf("Expected error %s, got %s", ErrTokenExpired, err) |
||||
} |
||||
|
||||
vids := vrs.List() |
||||
if len(vids) == 0 || len(vids) > 1 { |
||||
t.Fatalf("Unexpected number of vids %v", vids) |
||||
} |
||||
|
||||
if vids[0] != "err" { |
||||
t.Fatalf("Unexpected vid %v", vids[0]) |
||||
} |
||||
} |
@ -0,0 +1,68 @@ |
||||
/* |
||||
* 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 madmin |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/rand" |
||||
"io" |
||||
"io/ioutil" |
||||
|
||||
"github.com/minio/sio" |
||||
"golang.org/x/crypto/argon2" |
||||
) |
||||
|
||||
// EncryptData - encrypts server config data.
|
||||
func EncryptData(password string, data []byte) ([]byte, error) { |
||||
salt := make([]byte, 32) |
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// derive an encryption key from the master key and the nonce
|
||||
var key [32]byte |
||||
copy(key[:], argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)) |
||||
|
||||
encrypted, err := sio.EncryptReader(bytes.NewReader(data), sio.Config{ |
||||
Key: key[:]}, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
edata, err := ioutil.ReadAll(encrypted) |
||||
return append(salt, edata...), err |
||||
} |
||||
|
||||
// DecryptData - decrypts server config data.
|
||||
func DecryptData(password string, data io.Reader) ([]byte, error) { |
||||
salt := make([]byte, 32) |
||||
if _, err := io.ReadFull(data, salt); err != nil { |
||||
return nil, err |
||||
} |
||||
// derive an encryption key from the master key and the nonce
|
||||
var key [32]byte |
||||
copy(key[:], argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)) |
||||
|
||||
decrypted, err := sio.DecryptReader(data, sio.Config{ |
||||
Key: key[:]}, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return ioutil.ReadAll(decrypted) |
||||
} |
@ -0,0 +1,52 @@ |
||||
// +build ignore
|
||||
|
||||
/* |
||||
* Minio Cloud Storage, (C) 2017 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 main |
||||
|
||||
import ( |
||||
"log" |
||||
|
||||
"github.com/minio/minio/pkg/madmin" |
||||
) |
||||
|
||||
func main() { |
||||
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
|
||||
// dummy values, please replace them with original values.
|
||||
|
||||
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
|
||||
// dummy values, please replace them with original values.
|
||||
|
||||
// API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise.
|
||||
// New returns an Minio Admin client object.
|
||||
madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
if err = madmClnt.AddUser("newuser", "newstrongpassword"); err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
// Create policy
|
||||
policy := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": ["arn:aws:s3:::my-bucketname/*"],"Sid": ""}]}` |
||||
|
||||
if err = madmClnt.AddUserPolicy("newuser", policy); err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
} |
@ -0,0 +1,158 @@ |
||||
/* |
||||
* 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 madmin |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
"net/url" |
||||
) |
||||
|
||||
// AccountStatus - account status.
|
||||
type AccountStatus string |
||||
|
||||
// Account status per user.
|
||||
const ( |
||||
AccountEnabled AccountStatus = "enabled" |
||||
AccountDisabled AccountStatus = "disabled" |
||||
) |
||||
|
||||
// UserInfo carries information about long term users.
|
||||
type UserInfo struct { |
||||
SecretKey string `json:"secretKey"` |
||||
Status AccountStatus `json:"status"` |
||||
} |
||||
|
||||
// RemoveUser - remove a user.
|
||||
func (adm *AdminClient) RemoveUser(accessKey string) error { |
||||
queryValues := url.Values{} |
||||
queryValues.Set("accessKey", accessKey) |
||||
|
||||
reqData := requestData{ |
||||
relPath: "/v1/remove-user", |
||||
queryValues: queryValues, |
||||
} |
||||
|
||||
// Execute DELETE on /minio/admin/v1/remove-user to remove a user.
|
||||
resp, err := adm.executeMethod("DELETE", reqData) |
||||
|
||||
defer closeResponse(resp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return httpRespToErrorResponse(resp) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// SetUser - sets a user info.
|
||||
func (adm *AdminClient) SetUser(accessKey, secretKey string, status AccountStatus) error { |
||||
data, err := json.Marshal(UserInfo{ |
||||
SecretKey: secretKey, |
||||
Status: status, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
econfigBytes, err := EncryptData(adm.secretAccessKey, data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
queryValues := url.Values{} |
||||
queryValues.Set("accessKey", accessKey) |
||||
|
||||
reqData := requestData{ |
||||
relPath: "/v1/add-user", |
||||
queryValues: queryValues, |
||||
content: econfigBytes, |
||||
} |
||||
|
||||
// Execute PUT on /minio/admin/v1/add-user to set a user.
|
||||
resp, err := adm.executeMethod("PUT", reqData) |
||||
|
||||
defer closeResponse(resp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return httpRespToErrorResponse(resp) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// AddUser - adds a user.
|
||||
func (adm *AdminClient) AddUser(accessKey, secretKey string) error { |
||||
return adm.SetUser(accessKey, secretKey, AccountEnabled) |
||||
} |
||||
|
||||
// RemoveUserPolicy - remove a policy for a user.
|
||||
func (adm *AdminClient) RemoveUserPolicy(accessKey string) error { |
||||
queryValues := url.Values{} |
||||
queryValues.Set("accessKey", accessKey) |
||||
|
||||
reqData := requestData{ |
||||
relPath: "/v1/remove-user-policy", |
||||
queryValues: queryValues, |
||||
} |
||||
|
||||
// Execute DELETE on /minio/admin/v1/remove-user-policy to remove policy.
|
||||
resp, err := adm.executeMethod("DELETE", reqData) |
||||
|
||||
defer closeResponse(resp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return httpRespToErrorResponse(resp) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// AddUserPolicy - adds a policy for a user.
|
||||
func (adm *AdminClient) AddUserPolicy(accessKey, policy string) error { |
||||
queryValues := url.Values{} |
||||
queryValues.Set("accessKey", accessKey) |
||||
|
||||
reqData := requestData{ |
||||
relPath: "/v1/add-user-policy", |
||||
queryValues: queryValues, |
||||
content: []byte(policy), |
||||
} |
||||
|
||||
// Execute PUT on /minio/admin/v1/add-user-policy to set policy.
|
||||
resp, err := adm.executeMethod("PUT", reqData) |
||||
|
||||
defer closeResponse(resp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
return httpRespToErrorResponse(resp) |
||||
} |
||||
|
||||
return nil |
||||
} |
Loading…
Reference in new issue