Merge pull request #1080 from harshavardhana/bucket-policy
accessPolicy: Implement Put, Get, Delete access policy.master
commit
5ac4afa4d1
@ -1,71 +0,0 @@ |
|||||||
/* |
|
||||||
* Minio Cloud Storage, (C) 2015 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 "net/http" |
|
||||||
|
|
||||||
// Please read for more information - http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
|
||||||
//
|
|
||||||
// Here We are only supporting 'acl's through request headers not through their request body
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#setting-acls
|
|
||||||
|
|
||||||
// Minio only supports three types for now i.e 'private, public-read, public-read-write'
|
|
||||||
|
|
||||||
// ACLType - different acl types
|
|
||||||
type ACLType int |
|
||||||
|
|
||||||
const ( |
|
||||||
unsupportedACLType ACLType = iota |
|
||||||
privateACLType |
|
||||||
publicReadACLType |
|
||||||
publicReadWriteACLType |
|
||||||
) |
|
||||||
|
|
||||||
// Get acl type requested from 'x-amz-acl' header
|
|
||||||
func getACLType(req *http.Request) ACLType { |
|
||||||
aclHeader := req.Header.Get("x-amz-acl") |
|
||||||
if aclHeader != "" { |
|
||||||
switch { |
|
||||||
case aclHeader == "private": |
|
||||||
return privateACLType |
|
||||||
case aclHeader == "public-read": |
|
||||||
return publicReadACLType |
|
||||||
case aclHeader == "public-read-write": |
|
||||||
return publicReadWriteACLType |
|
||||||
default: |
|
||||||
return unsupportedACLType |
|
||||||
} |
|
||||||
} |
|
||||||
// make it default private
|
|
||||||
return privateACLType |
|
||||||
} |
|
||||||
|
|
||||||
// ACL type to human readable string
|
|
||||||
func getACLTypeString(acl ACLType) string { |
|
||||||
switch acl { |
|
||||||
case privateACLType: |
|
||||||
return "private" |
|
||||||
case publicReadACLType: |
|
||||||
return "public-read" |
|
||||||
case publicReadWriteACLType: |
|
||||||
return "public-read-write" |
|
||||||
case unsupportedACLType: |
|
||||||
return "" |
|
||||||
default: |
|
||||||
return "private" |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,283 @@ |
|||||||
|
/* |
||||||
|
* Minio Cloud Storage, (C) 2015, 2016 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/sha256" |
||||||
|
"encoding/hex" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
|
||||||
|
mux "github.com/gorilla/mux" |
||||||
|
"github.com/minio/minio/pkg/fs" |
||||||
|
"github.com/minio/minio/pkg/probe" |
||||||
|
"github.com/minio/minio/pkg/s3/access" |
||||||
|
) |
||||||
|
|
||||||
|
// maximum supported access policy size.
|
||||||
|
const maxAccessPolicySize = 20 * 1024 * 1024 // 20KiB.
|
||||||
|
|
||||||
|
// Verify if a given action is valid for the url path based on the
|
||||||
|
// existing bucket access policy.
|
||||||
|
func bucketPolicyEvalStatements(action string, resource string, conditions map[string]string, statements []accesspolicy.Statement) bool { |
||||||
|
for _, statement := range statements { |
||||||
|
if bucketPolicyMatchStatement(action, resource, conditions, statement) { |
||||||
|
if statement.Effect == "Allow" { |
||||||
|
return true |
||||||
|
} |
||||||
|
// else statement.Effect == "Deny"
|
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
// None match so deny.
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Verify if action, resource and conditions match input policy statement.
|
||||||
|
func bucketPolicyMatchStatement(action string, resource string, conditions map[string]string, statement accesspolicy.Statement) bool { |
||||||
|
// Verify if action matches.
|
||||||
|
if bucketPolicyActionMatch(action, statement) { |
||||||
|
// Verify if resource matches.
|
||||||
|
if bucketPolicyResourceMatch(resource, statement) { |
||||||
|
// Verify if condition matches.
|
||||||
|
if bucketPolicyConditionMatch(conditions, statement) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Verify if given action matches with policy statement.
|
||||||
|
func bucketPolicyActionMatch(action string, statement accesspolicy.Statement) bool { |
||||||
|
for _, policyAction := range statement.Actions { |
||||||
|
// Policy action can be a regex, validate the action with matching string.
|
||||||
|
matched, e := regexp.MatchString(policyAction, action) |
||||||
|
fatalIf(probe.NewError(e), "Invalid pattern, please verify the pattern string.", nil) |
||||||
|
if matched { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Verify if given resource matches with policy statement.
|
||||||
|
func bucketPolicyResourceMatch(resource string, statement accesspolicy.Statement) bool { |
||||||
|
for _, presource := range statement.Resources { |
||||||
|
matched, e := regexp.MatchString(presource, strings.TrimPrefix(resource, "/")) |
||||||
|
fatalIf(probe.NewError(e), "Invalid pattern, please verify the pattern string.", nil) |
||||||
|
// For any path matches, we return quickly and the let the caller continue.
|
||||||
|
if matched { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Verify if given condition matches with policy statement.
|
||||||
|
func bucketPolicyConditionMatch(conditions map[string]string, statement accesspolicy.Statement) bool { |
||||||
|
// Supports following conditions.
|
||||||
|
// - StringEquals
|
||||||
|
// - StringNotEquals
|
||||||
|
//
|
||||||
|
// Supported applicable condition keys for each conditions.
|
||||||
|
// - s3:prefix
|
||||||
|
// - s3:max-keys
|
||||||
|
var conditionMatches = true |
||||||
|
for condition, conditionKeys := range statement.Conditions { |
||||||
|
if condition == "StringEquals" { |
||||||
|
if conditionKeys["s3:prefix"] != conditions["prefix"] { |
||||||
|
conditionMatches = false |
||||||
|
break |
||||||
|
} |
||||||
|
if conditionKeys["s3:max-keys"] != conditions["max-keys"] { |
||||||
|
conditionMatches = false |
||||||
|
break |
||||||
|
} |
||||||
|
} else if condition == "StringNotEquals" { |
||||||
|
if conditionKeys["s3:prefix"] == conditions["prefix"] { |
||||||
|
conditionMatches = false |
||||||
|
break |
||||||
|
} |
||||||
|
if conditionKeys["s3:max-keys"] == conditions["max-keys"] { |
||||||
|
conditionMatches = false |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return conditionMatches |
||||||
|
} |
||||||
|
|
||||||
|
// PutBucketPolicyHandler - PUT Bucket policy
|
||||||
|
// -----------------
|
||||||
|
// This implementation of the PUT operation uses the policy
|
||||||
|
// subresource to add to or replace a policy on a bucket
|
||||||
|
func (api storageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
vars := mux.Vars(r) |
||||||
|
bucket := vars["bucket"] |
||||||
|
|
||||||
|
// If Content-Length is unknown or zero, deny the
|
||||||
|
// request. PutBucketPolicy always needs a Content-Length if
|
||||||
|
// incoming request is not chunked.
|
||||||
|
if !contains(r.TransferEncoding, "chunked") { |
||||||
|
if r.ContentLength == -1 || r.ContentLength == 0 { |
||||||
|
writeErrorResponse(w, r, MissingContentLength, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
// If Content-Length is greater than maximum allowed policy size.
|
||||||
|
if r.ContentLength > maxAccessPolicySize { |
||||||
|
writeErrorResponse(w, r, EntityTooLarge, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Read access policy up to maxAccessPolicySize.
|
||||||
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||||
|
// bucket policies are limited to 20KB in size, using a limit reader.
|
||||||
|
accessPolicyBytes, e := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize)) |
||||||
|
if e != nil { |
||||||
|
errorIf(probe.NewError(e).Trace(bucket), "Reading policy failed.", nil) |
||||||
|
writeErrorResponse(w, r, InternalError, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Parse access access.
|
||||||
|
accessPolicy, e := accesspolicy.Validate(accessPolicyBytes) |
||||||
|
if e != nil { |
||||||
|
writeErrorResponse(w, r, InvalidPolicyDocument, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// If the policy resource has different bucket name, reject it.
|
||||||
|
for _, statement := range accessPolicy.Statements { |
||||||
|
for _, resource := range statement.Resources { |
||||||
|
resourcePrefix := strings.SplitAfter(resource, accesspolicy.AWSResourcePrefix)[1] |
||||||
|
if !strings.HasPrefix(resourcePrefix, bucket) { |
||||||
|
writeErrorResponse(w, r, MalformedPolicy, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set http request for signature verification.
|
||||||
|
auth := api.Signature.SetHTTPRequestToVerify(r) |
||||||
|
if isRequestPresignedSignatureV4(r) { |
||||||
|
ok, err := auth.DoesPresignedSignatureMatch() |
||||||
|
if err != nil { |
||||||
|
errorIf(err.Trace(r.URL.String()), "Presigned signature verification failed.", nil) |
||||||
|
writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
if !ok { |
||||||
|
writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
} else if isRequestSignatureV4(r) { |
||||||
|
sh := sha256.New() |
||||||
|
sh.Write(accessPolicyBytes) |
||||||
|
ok, err := api.Signature.DoesSignatureMatch(hex.EncodeToString(sh.Sum(nil))) |
||||||
|
if err != nil { |
||||||
|
errorIf(err.Trace(string(accessPolicyBytes)), "SaveBucketPolicy failed.", nil) |
||||||
|
writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
if !ok { |
||||||
|
writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Save bucket policy.
|
||||||
|
err := writeBucketPolicy(bucket, accessPolicyBytes) |
||||||
|
if err != nil { |
||||||
|
errorIf(err.Trace(bucket), "SaveBucketPolicy failed.", nil) |
||||||
|
switch err.ToGoError().(type) { |
||||||
|
case fs.BucketNameInvalid: |
||||||
|
writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) |
||||||
|
default: |
||||||
|
writeErrorResponse(w, r, InternalError, r.URL.Path) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
writeSuccessNoContent(w) |
||||||
|
} |
||||||
|
|
||||||
|
// DeleteBucketPolicyHandler - DELETE Bucket policy
|
||||||
|
// -----------------
|
||||||
|
// This implementation of the DELETE operation uses the policy
|
||||||
|
// subresource to add to remove a policy on a bucket.
|
||||||
|
func (api storageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
vars := mux.Vars(r) |
||||||
|
bucket := vars["bucket"] |
||||||
|
|
||||||
|
// Validate incoming signature.
|
||||||
|
if match, s3Error := isSignV4ReqAuthenticated(api.Signature, r); !match { |
||||||
|
writeErrorResponse(w, r, s3Error, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Delete bucket access policy.
|
||||||
|
err := removeBucketPolicy(bucket) |
||||||
|
if err != nil { |
||||||
|
errorIf(err.Trace(bucket), "DeleteBucketPolicy failed.", nil) |
||||||
|
switch err.ToGoError().(type) { |
||||||
|
case fs.BucketNameInvalid: |
||||||
|
writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) |
||||||
|
case fs.BucketPolicyNotFound: |
||||||
|
writeErrorResponse(w, r, NoSuchBucketPolicy, r.URL.Path) |
||||||
|
default: |
||||||
|
writeErrorResponse(w, r, InternalError, r.URL.Path) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
writeSuccessNoContent(w) |
||||||
|
} |
||||||
|
|
||||||
|
// GetBucketPolicyHandler - GET Bucket policy
|
||||||
|
// -----------------
|
||||||
|
// This operation uses the policy
|
||||||
|
// subresource to return the policy of a specified bucket.
|
||||||
|
func (api storageAPI) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { |
||||||
|
vars := mux.Vars(r) |
||||||
|
bucket := vars["bucket"] |
||||||
|
|
||||||
|
// Validate incoming signature.
|
||||||
|
if match, s3Error := isSignV4ReqAuthenticated(api.Signature, r); !match { |
||||||
|
writeErrorResponse(w, r, s3Error, r.URL.Path) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Read bucket access policy.
|
||||||
|
p, err := readBucketPolicy(bucket) |
||||||
|
if err != nil { |
||||||
|
errorIf(err.Trace(bucket), "GetBucketPolicy failed.", nil) |
||||||
|
switch err.ToGoError().(type) { |
||||||
|
case fs.BucketNameInvalid: |
||||||
|
writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) |
||||||
|
case fs.BucketPolicyNotFound: |
||||||
|
writeErrorResponse(w, r, NoSuchBucketPolicy, r.URL.Path) |
||||||
|
default: |
||||||
|
writeErrorResponse(w, r, InternalError, r.URL.Path) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
io.Copy(w, bytes.NewReader(p)) |
||||||
|
} |
@ -0,0 +1,147 @@ |
|||||||
|
/* |
||||||
|
* Minio Cloud Storage, (C) 2015, 2016 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 ( |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/fs" |
||||||
|
"github.com/minio/minio/pkg/probe" |
||||||
|
) |
||||||
|
|
||||||
|
// getBucketsConfigPath - get buckets path.
|
||||||
|
func getBucketsConfigPath() (string, *probe.Error) { |
||||||
|
configPath, err := getConfigPath() |
||||||
|
if err != nil { |
||||||
|
return "", err.Trace() |
||||||
|
} |
||||||
|
return filepath.Join(configPath, "buckets"), nil |
||||||
|
} |
||||||
|
|
||||||
|
// createBucketsConfigPath - create buckets directory.
|
||||||
|
func createBucketsConfigPath() *probe.Error { |
||||||
|
bucketsConfigPath, err := getBucketsConfigPath() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if e := os.MkdirAll(bucketsConfigPath, 0700); e != nil { |
||||||
|
return probe.NewError(e) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// getBucketConfigPath - get bucket path.
|
||||||
|
func getBucketConfigPath(bucket string) (string, *probe.Error) { |
||||||
|
bucketsConfigPath, err := getBucketsConfigPath() |
||||||
|
if err != nil { |
||||||
|
return "", err.Trace() |
||||||
|
} |
||||||
|
return filepath.Join(bucketsConfigPath, bucket), nil |
||||||
|
} |
||||||
|
|
||||||
|
// createBucketConfigPath - create bucket directory.
|
||||||
|
func createBucketConfigPath(bucket string) *probe.Error { |
||||||
|
bucketConfigPath, err := getBucketConfigPath(bucket) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if e := os.MkdirAll(bucketConfigPath, 0700); e != nil { |
||||||
|
return probe.NewError(e) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// readBucketPolicy - read bucket policy.
|
||||||
|
func readBucketPolicy(bucket string) ([]byte, *probe.Error) { |
||||||
|
// Verify bucket is valid.
|
||||||
|
if !fs.IsValidBucketName(bucket) { |
||||||
|
return nil, probe.NewError(fs.BucketNameInvalid{Bucket: bucket}) |
||||||
|
} |
||||||
|
|
||||||
|
bucketConfigPath, err := getBucketConfigPath(bucket) |
||||||
|
if err != nil { |
||||||
|
return nil, err.Trace() |
||||||
|
} |
||||||
|
|
||||||
|
// Get policy file.
|
||||||
|
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json") |
||||||
|
if _, e := os.Stat(bucketPolicyFile); e != nil { |
||||||
|
if os.IsNotExist(e) { |
||||||
|
return nil, probe.NewError(fs.BucketPolicyNotFound{Bucket: bucket}) |
||||||
|
} |
||||||
|
return nil, probe.NewError(e) |
||||||
|
} |
||||||
|
|
||||||
|
accessPolicyBytes, e := ioutil.ReadFile(bucketPolicyFile) |
||||||
|
if e != nil { |
||||||
|
return nil, probe.NewError(e) |
||||||
|
} |
||||||
|
return accessPolicyBytes, nil |
||||||
|
} |
||||||
|
|
||||||
|
// removeBucketPolicy - remove bucket policy.
|
||||||
|
func removeBucketPolicy(bucket string) *probe.Error { |
||||||
|
// Verify bucket is valid.
|
||||||
|
if !fs.IsValidBucketName(bucket) { |
||||||
|
return probe.NewError(fs.BucketNameInvalid{Bucket: bucket}) |
||||||
|
} |
||||||
|
|
||||||
|
bucketConfigPath, err := getBucketConfigPath(bucket) |
||||||
|
if err != nil { |
||||||
|
return err.Trace(bucket) |
||||||
|
} |
||||||
|
|
||||||
|
// Get policy file.
|
||||||
|
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json") |
||||||
|
if _, e := os.Stat(bucketPolicyFile); e != nil { |
||||||
|
if os.IsNotExist(e) { |
||||||
|
return probe.NewError(fs.BucketPolicyNotFound{Bucket: bucket}) |
||||||
|
} |
||||||
|
return probe.NewError(e) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// writeBucketPolicy - save bucket policy.
|
||||||
|
func writeBucketPolicy(bucket string, accessPolicyBytes []byte) *probe.Error { |
||||||
|
// Verify if bucket path legal
|
||||||
|
if !fs.IsValidBucketName(bucket) { |
||||||
|
return probe.NewError(fs.BucketNameInvalid{Bucket: bucket}) |
||||||
|
} |
||||||
|
|
||||||
|
bucketConfigPath, err := getBucketConfigPath(bucket) |
||||||
|
if err != nil { |
||||||
|
return err.Trace() |
||||||
|
} |
||||||
|
|
||||||
|
// Get policy file.
|
||||||
|
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json") |
||||||
|
if _, e := os.Stat(bucketPolicyFile); e != nil { |
||||||
|
if !os.IsNotExist(e) { |
||||||
|
return probe.NewError(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Write bucket policy.
|
||||||
|
if e := ioutil.WriteFile(bucketPolicyFile, accessPolicyBytes, 0600); e != nil { |
||||||
|
return probe.NewError(e) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -1,96 +0,0 @@ |
|||||||
/* |
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 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 fs |
|
||||||
|
|
||||||
// IsPrivateBucket - is private bucket
|
|
||||||
func (fs Filesystem) IsPrivateBucket(bucket string) bool { |
|
||||||
fs.rwLock.RLock() |
|
||||||
defer fs.rwLock.RUnlock() |
|
||||||
bucketMetadata, ok := fs.buckets.Metadata[bucket] |
|
||||||
if !ok { |
|
||||||
return true |
|
||||||
} |
|
||||||
return bucketMetadata.ACL.IsPrivate() |
|
||||||
} |
|
||||||
|
|
||||||
// IsPublicBucket - is public bucket
|
|
||||||
func (fs Filesystem) IsPublicBucket(bucket string) bool { |
|
||||||
fs.rwLock.RLock() |
|
||||||
defer fs.rwLock.RUnlock() |
|
||||||
bucketMetadata, ok := fs.buckets.Metadata[bucket] |
|
||||||
if !ok { |
|
||||||
return true |
|
||||||
} |
|
||||||
return bucketMetadata.ACL.IsPublicReadWrite() |
|
||||||
} |
|
||||||
|
|
||||||
// IsReadOnlyBucket - is read only bucket
|
|
||||||
func (fs Filesystem) IsReadOnlyBucket(bucket string) bool { |
|
||||||
fs.rwLock.RLock() |
|
||||||
defer fs.rwLock.RUnlock() |
|
||||||
bucketMetadata, ok := fs.buckets.Metadata[bucket] |
|
||||||
if !ok { |
|
||||||
return true |
|
||||||
} |
|
||||||
return bucketMetadata.ACL.IsPublicRead() |
|
||||||
} |
|
||||||
|
|
||||||
// BucketACL - bucket level access control
|
|
||||||
type BucketACL string |
|
||||||
|
|
||||||
// different types of ACL's currently supported for buckets
|
|
||||||
const ( |
|
||||||
BucketPrivate = BucketACL("private") |
|
||||||
BucketPublicRead = BucketACL("public-read") |
|
||||||
BucketPublicReadWrite = BucketACL("public-read-write") |
|
||||||
) |
|
||||||
|
|
||||||
func (b BucketACL) String() string { |
|
||||||
return string(b) |
|
||||||
} |
|
||||||
|
|
||||||
// IsPrivate - is acl Private
|
|
||||||
func (b BucketACL) IsPrivate() bool { |
|
||||||
return b == BucketACL("private") |
|
||||||
} |
|
||||||
|
|
||||||
// IsPublicRead - is acl PublicRead
|
|
||||||
func (b BucketACL) IsPublicRead() bool { |
|
||||||
return b == BucketACL("public-read") |
|
||||||
} |
|
||||||
|
|
||||||
// IsPublicReadWrite - is acl PublicReadWrite
|
|
||||||
func (b BucketACL) IsPublicReadWrite() bool { |
|
||||||
return b == BucketACL("public-read-write") |
|
||||||
} |
|
||||||
|
|
||||||
// IsValidBucketACL - is provided acl string supported
|
|
||||||
func IsValidBucketACL(acl string) bool { |
|
||||||
switch acl { |
|
||||||
case "private": |
|
||||||
fallthrough |
|
||||||
case "public-read": |
|
||||||
fallthrough |
|
||||||
case "public-read-write": |
|
||||||
return true |
|
||||||
case "": |
|
||||||
// by default its "private"
|
|
||||||
return true |
|
||||||
default: |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,33 @@ |
|||||||
|
## Access Policy |
||||||
|
|
||||||
|
This package implements parsing and validating bucket access policies based on Access Policy Language specification - http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html |
||||||
|
|
||||||
|
### Supports following effects. |
||||||
|
|
||||||
|
Allow |
||||||
|
Deny |
||||||
|
|
||||||
|
### Supports following set of operations. |
||||||
|
|
||||||
|
* |
||||||
|
s3:* |
||||||
|
s3:GetObject |
||||||
|
s3:ListBucket |
||||||
|
s3:PutObject |
||||||
|
s3:CreateBucket |
||||||
|
s3:GetBucketLocation |
||||||
|
s3:DeleteBucket |
||||||
|
s3:DeleteObject |
||||||
|
s3:AbortMultipartUpload |
||||||
|
s3:ListBucketMultipartUploads |
||||||
|
s3:ListMultipartUploadParts |
||||||
|
|
||||||
|
### Supports following conditions. |
||||||
|
|
||||||
|
StringEquals |
||||||
|
StringNotEquals |
||||||
|
|
||||||
|
Supported applicable condition keys for each conditions. |
||||||
|
|
||||||
|
s3:prefix |
||||||
|
s3:max-keys |
@ -0,0 +1,230 @@ |
|||||||
|
/* |
||||||
|
* Minio Cloud Storage, (C) 2015, 2016 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 accesspolicy implements AWS Access Policy Language parser in
|
||||||
|
// accordance with http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||||
|
package accesspolicy |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// AWSResourcePrefix - bucket policy resource prefix.
|
||||||
|
AWSResourcePrefix = "arn:aws:s3:::" |
||||||
|
) |
||||||
|
|
||||||
|
// supportedActionMap - lists all the actions supported by minio.
|
||||||
|
var supportedActionMap = map[string]struct{}{ |
||||||
|
"*": {}, |
||||||
|
"s3:*": {}, |
||||||
|
"s3:GetObject": {}, |
||||||
|
"s3:ListBucket": {}, |
||||||
|
"s3:PutObject": {}, |
||||||
|
"s3:CreateBucket": {}, |
||||||
|
"s3:GetBucketLocation": {}, |
||||||
|
"s3:DeleteBucket": {}, |
||||||
|
"s3:DeleteObject": {}, |
||||||
|
"s3:AbortMultipartUpload": {}, |
||||||
|
"s3:ListBucketMultipartUploads": {}, |
||||||
|
"s3:ListMultipartUploadParts": {}, |
||||||
|
} |
||||||
|
|
||||||
|
// User - canonical users list.
|
||||||
|
type User struct { |
||||||
|
AWS []string |
||||||
|
} |
||||||
|
|
||||||
|
// Statement - minio policy statement
|
||||||
|
type Statement struct { |
||||||
|
Sid string |
||||||
|
Effect string |
||||||
|
Principal User |
||||||
|
Actions []string `json:"Action"` |
||||||
|
Resources []string `json:"Resource"` |
||||||
|
Conditions map[string]map[string]string `json:"Condition"` |
||||||
|
} |
||||||
|
|
||||||
|
// BucketPolicy - minio policy collection
|
||||||
|
type BucketPolicy struct { |
||||||
|
Version string // date in 0000-00-00 format
|
||||||
|
Statements []Statement `json:"Statement"` |
||||||
|
} |
||||||
|
|
||||||
|
// supportedEffectMap - supported effects.
|
||||||
|
var supportedEffectMap = map[string]struct{}{ |
||||||
|
"Allow": {}, |
||||||
|
"Deny": {}, |
||||||
|
} |
||||||
|
|
||||||
|
// isValidActions - are actions valid.
|
||||||
|
func isValidActions(actions []string) (err error) { |
||||||
|
// Statement actions cannot be empty.
|
||||||
|
if len(actions) == 0 { |
||||||
|
err = errors.New("Action list cannot be empty.") |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, action := range actions { |
||||||
|
if _, ok := supportedActionMap[action]; !ok { |
||||||
|
err = errors.New("Unsupported action found: ‘" + action + "’, please validate your policy document.") |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// isValidEffect - is effect valid.
|
||||||
|
func isValidEffect(effect string) error { |
||||||
|
// Statement effect cannot be empty.
|
||||||
|
if len(effect) == 0 { |
||||||
|
err := errors.New("Policy effect cannot be empty.") |
||||||
|
return err |
||||||
|
} |
||||||
|
_, ok := supportedEffectMap[effect] |
||||||
|
if !ok { |
||||||
|
err := errors.New("Unsupported Effect found: ‘" + effect + "’, please validate your policy document.") |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// isValidResources - are valid resources.
|
||||||
|
func isValidResources(resources []string) (err error) { |
||||||
|
// Statement resources cannot be empty.
|
||||||
|
if len(resources) == 0 { |
||||||
|
err = errors.New("Resource list cannot be empty.") |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, resource := range resources { |
||||||
|
if !strings.HasPrefix(resource, AWSResourcePrefix) { |
||||||
|
err = errors.New("Unsupported resource style found: ‘" + resource + "’, please validate your policy document.") |
||||||
|
return err |
||||||
|
} |
||||||
|
resourceSuffix := strings.SplitAfter(resource, AWSResourcePrefix)[1] |
||||||
|
if len(resourceSuffix) == 0 || strings.HasPrefix(resourceSuffix, "/") { |
||||||
|
err = errors.New("Invalid resource style found: ‘" + resource + "’, please validate your policy document.") |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// isValidPrincipals - are valid principals.
|
||||||
|
func isValidPrincipals(principals []string) (err error) { |
||||||
|
// Statement principal should have a value.
|
||||||
|
if len(principals) == 0 { |
||||||
|
err = errors.New("Principal cannot be empty.") |
||||||
|
return err |
||||||
|
} |
||||||
|
var ok bool |
||||||
|
for _, principal := range principals { |
||||||
|
// Minio does not support or implement IAM, "*" is the only valid value.
|
||||||
|
if principal == "*" { |
||||||
|
ok = true |
||||||
|
continue |
||||||
|
} |
||||||
|
ok = false |
||||||
|
} |
||||||
|
if !ok { |
||||||
|
err = errors.New("Unsupported principal style found: ‘" + strings.Join(principals, " ") + "’, please validate your policy document.") |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func isValidConditions(conditions map[string]map[string]string) (err error) { |
||||||
|
// Verify conditions should be valid.
|
||||||
|
if len(conditions) > 0 { |
||||||
|
// Validate if stringEquals, stringNotEquals are present
|
||||||
|
// if not throw an error.
|
||||||
|
_, stringEqualsOK := conditions["StringEquals"] |
||||||
|
_, stringNotEqualsOK := conditions["StringNotEquals"] |
||||||
|
if !stringEqualsOK && !stringNotEqualsOK { |
||||||
|
err = fmt.Errorf("Unsupported condition type found: ‘%s’, please validate your policy document.", conditions) |
||||||
|
return err |
||||||
|
} |
||||||
|
// Validate s3:prefix, s3:max-keys are present if not
|
||||||
|
// throw an error.
|
||||||
|
if len(conditions["StringEquals"]) > 0 { |
||||||
|
_, s3PrefixOK := conditions["StringEquals"]["s3:prefix"] |
||||||
|
_, s3MaxKeysOK := conditions["StringEquals"]["s3:max-keys"] |
||||||
|
if !s3PrefixOK && !s3MaxKeysOK { |
||||||
|
err = fmt.Errorf("Unsupported condition keys found: ‘%s’, please validate your policy document.", |
||||||
|
conditions["StringEquals"]) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
if len(conditions["StringNotEquals"]) > 0 { |
||||||
|
_, s3PrefixOK := conditions["StringNotEquals"]["s3:prefix"] |
||||||
|
_, s3MaxKeysOK := conditions["StringNotEquals"]["s3:max-keys"] |
||||||
|
if !s3PrefixOK && !s3MaxKeysOK { |
||||||
|
err = fmt.Errorf("Unsupported condition keys found: ‘%s’, please validate your policy document.", |
||||||
|
conditions["StringNotEquals"]) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Validate - validate if request body is of proper JSON and in
|
||||||
|
// accordance with policy standards.
|
||||||
|
func Validate(bucketPolicyBuf []byte) (policy BucketPolicy, err error) { |
||||||
|
if err = json.Unmarshal(bucketPolicyBuf, &policy); err != nil { |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
|
||||||
|
// Policy version cannot be empty.
|
||||||
|
if len(policy.Version) == 0 { |
||||||
|
err = errors.New("Policy version cannot be empty.") |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
|
||||||
|
// Policy statements cannot be empty.
|
||||||
|
if len(policy.Statements) == 0 { |
||||||
|
err = errors.New("Policy statement cannot be empty.") |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
|
||||||
|
// Loop through all policy statements and validate entries.
|
||||||
|
for _, statement := range policy.Statements { |
||||||
|
// Statement effect should be valid.
|
||||||
|
if err := isValidEffect(statement.Effect); err != nil { |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
// Statement principal should be supported format.
|
||||||
|
if err := isValidPrincipals(statement.Principal.AWS); err != nil { |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
// Statement actions should be valid.
|
||||||
|
if err := isValidActions(statement.Actions); err != nil { |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
// Statment resources should be valid.
|
||||||
|
if err := isValidResources(statement.Resources); err != nil { |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
// Statement conditions should be valid.
|
||||||
|
if err := isValidConditions(statement.Conditions); err != nil { |
||||||
|
return BucketPolicy{}, err |
||||||
|
} |
||||||
|
} |
||||||
|
// Return successfully parsed policy structure.
|
||||||
|
return policy, nil |
||||||
|
} |
Loading…
Reference in new issue