This patch implements Get,Put,Delete bucket policies Supporting - http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html Currently supports following actions. "*": true, "s3:*": true, "s3:GetObject": true, "s3:ListBucket": true, "s3:PutObject": true, "s3:CreateBucket": true, "s3:GetBucketLocation": true, "s3:DeleteBucket": true, "s3:DeleteObject": true, "s3:AbortMultipartUpload": true, "s3:ListBucketMultipartUploads": true, "s3:ListMultipartUploadParts": true, following conditions for "StringEquals" and "StringNotEquals" "s3:prefix", "s3:max-keys"master
parent
846410c563
commit
d5057b3c51
@ -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