diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/credentials/file_minio_client.go | 18 | ||||
-rw-r--r-- | pkg/credentials/iam_aws.go | 54 | ||||
-rw-r--r-- | pkg/credentials/iam_aws_test.go | 46 | ||||
-rw-r--r-- | pkg/policy/bucket-policy.go | 10 | ||||
-rw-r--r-- | pkg/policy/bucket-policy_test.go | 8 | ||||
-rw-r--r-- | pkg/s3utils/utils.go | 31 |
6 files changed, 148 insertions, 19 deletions
diff --git a/pkg/credentials/file_minio_client.go b/pkg/credentials/file_minio_client.go index c282c2a..6a6827e 100644 --- a/pkg/credentials/file_minio_client.go +++ b/pkg/credentials/file_minio_client.go @@ -62,13 +62,17 @@ func NewFileMinioClient(filename string, alias string) *Credentials { // users home directory. func (p *FileMinioClient) Retrieve() (Value, error) { if p.filename == "" { - homeDir, err := homedir.Dir() - if err != nil { - return Value{}, err - } - p.filename = filepath.Join(homeDir, ".mc", "config.json") - if runtime.GOOS == "windows" { - p.filename = filepath.Join(homeDir, "mc", "config.json") + if value, ok := os.LookupEnv("MINIO_SHARED_CREDENTIALS_FILE"); ok { + p.filename = value + } else { + homeDir, err := homedir.Dir() + if err != nil { + return Value{}, err + } + p.filename = filepath.Join(homeDir, ".mc", "config.json") + if runtime.GOOS == "windows" { + p.filename = filepath.Join(homeDir, "mc", "config.json") + } } } diff --git a/pkg/credentials/iam_aws.go b/pkg/credentials/iam_aws.go index 637df74..6845c9a 100644 --- a/pkg/credentials/iam_aws.go +++ b/pkg/credentials/iam_aws.go @@ -21,8 +21,10 @@ import ( "bufio" "encoding/json" "errors" + "fmt" "net/http" "net/url" + "os" "path" "time" ) @@ -50,16 +52,25 @@ type IAM struct { // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html const ( defaultIAMRoleEndpoint = "http://169.254.169.254" + defaultECSRoleEndpoint = "http://169.254.170.2" defaultIAMSecurityCredsPath = "/latest/meta-data/iam/security-credentials" ) +// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +func getEndpoint(endpoint string) (string, bool) { + if endpoint != "" { + return endpoint, os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" + } + if ecsURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"); ecsURI != "" { + return fmt.Sprintf("%s%s", defaultECSRoleEndpoint, ecsURI), true + } + return defaultIAMRoleEndpoint, false +} + // NewIAM returns a pointer to a new Credentials object wrapping // the IAM. Takes a ConfigProvider to create a EC2Metadata client. // The ConfigProvider is satisfied by the session.Session type. func NewIAM(endpoint string) *Credentials { - if endpoint == "" { - endpoint = defaultIAMRoleEndpoint - } p := &IAM{ Client: &http.Client{ Transport: http.DefaultTransport, @@ -73,11 +84,17 @@ func NewIAM(endpoint string) *Credentials { // Error will be returned if the request fails, or unable to extract // the desired func (m *IAM) Retrieve() (Value, error) { - roleCreds, err := getCredentials(m.Client, m.endpoint) + endpoint, isEcsTask := getEndpoint(m.endpoint) + var roleCreds ec2RoleCredRespBody + var err error + if isEcsTask { + roleCreds, err = getEcsTaskCredentials(m.Client, endpoint) + } else { + roleCreds, err = getCredentials(m.Client, endpoint) + } if err != nil { return Value{}, err } - // Expiry window is set to 10secs. m.SetExpiration(roleCreds.Expiration, DefaultExpiryWindow) @@ -111,9 +128,6 @@ type ec2RoleCredRespBody struct { // be sent to fetch the rolling access credentials. // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html func getIAMRoleURL(endpoint string) (*url.URL, error) { - if endpoint == "" { - endpoint = defaultIAMRoleEndpoint - } u, err := url.Parse(endpoint) if err != nil { return nil, err @@ -153,12 +167,36 @@ func listRoleNames(client *http.Client, u *url.URL) ([]string, error) { return credsList, nil } +func getEcsTaskCredentials(client *http.Client, endpoint string) (ec2RoleCredRespBody, error) { + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return ec2RoleCredRespBody{}, err + } + + resp, err := client.Do(req) + if err != nil { + return ec2RoleCredRespBody{}, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return ec2RoleCredRespBody{}, errors.New(resp.Status) + } + + respCreds := ec2RoleCredRespBody{} + if err := json.NewDecoder(resp.Body).Decode(&respCreds); err != nil { + return ec2RoleCredRespBody{}, err + } + + return respCreds, nil +} + // getCredentials - obtains the credentials from the IAM role name associated with // the current EC2 service. // // If the credentials cannot be found, or there is an error // reading the response an error will be returned. func getCredentials(client *http.Client, endpoint string) (ec2RoleCredRespBody, error) { + // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html u, err := getIAMRoleURL(endpoint) if err != nil { diff --git a/pkg/credentials/iam_aws_test.go b/pkg/credentials/iam_aws_test.go index 86ea66b..4dbbb0a 100644 --- a/pkg/credentials/iam_aws_test.go +++ b/pkg/credentials/iam_aws_test.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" "testing" "time" ) @@ -41,6 +42,13 @@ const credsFailRespTmpl = `{ "LastUpdated": "2009-11-23T0:00:00Z" }` +const credsRespEcsTaskTmpl = `{ + "AccessKeyId" : "accessKey", + "SecretAccessKey" : "secret", + "Token" : "token", + "Expiration" : "%s" +}` + func initTestFailServer() *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not allowed", http.StatusBadRequest) @@ -73,6 +81,14 @@ func initTestServer(expireOn string, failAssume bool) *httptest.Server { return server } +func initEcsTaskTestServer(expireOn string) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, credsRespEcsTaskTmpl, expireOn) + })) + + return server +} + func TestIAMMalformedEndpoint(t *testing.T) { creds := NewIAM("%%%%") _, err := creds.Get() @@ -195,3 +211,33 @@ func TestIAMIsExpired(t *testing.T) { t.Error("Expected creds to be expired when curren time has changed") } } + +func TestEcsTask(t *testing.T) { + server := initEcsTaskTestServer("2014-12-16T01:51:37Z") + defer server.Close() + p := &IAM{ + Client: http.DefaultClient, + endpoint: server.URL, + } + os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials?id=task_credential_id") + creds, err := p.Retrieve() + os.Unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") + if err != nil { + t.Errorf("Unexpected failure %s", err) + } + if "accessKey" != creds.AccessKeyID { + t.Errorf("Expected \"accessKey\", got %s", creds.AccessKeyID) + } + + if "secret" != creds.SecretAccessKey { + t.Errorf("Expected \"secret\", got %s", creds.SecretAccessKey) + } + + if "token" != creds.SessionToken { + t.Errorf("Expected \"token\", got %s", creds.SessionToken) + } + + if !p.IsExpired() { + t.Error("Expected creds to be expired.") + } +} diff --git a/pkg/policy/bucket-policy.go b/pkg/policy/bucket-policy.go index 9d5f5b3..79fd801 100644 --- a/pkg/policy/bucket-policy.go +++ b/pkg/policy/bucket-policy.go @@ -557,7 +557,6 @@ func GetPolicy(statements []Statement, bucketName string, prefix string) BucketP } else { matchedObjResources = s.Resources.FuncMatch(resourceMatch, objectResource) } - if !matchedObjResources.IsEmpty() { readOnly, writeOnly := getObjectPolicy(s) for resource := range matchedObjResources { @@ -571,7 +570,8 @@ func GetPolicy(statements []Statement, bucketName string, prefix string) BucketP matchedResource = resource } } - } else if s.Resources.Contains(bucketResource) { + } + if s.Resources.Contains(bucketResource) { commonFound, readOnly, writeOnly := getBucketPolicy(s, prefix) bucketCommonFound = bucketCommonFound || commonFound bucketReadOnly = bucketReadOnly || readOnly @@ -605,6 +605,7 @@ func GetPolicies(statements []Statement, bucketName, prefix string) map[string]B } } } + // Pretend that policy resource as an actual object and fetch its policy for r := range objResources { // Put trailing * if exists in asterisk @@ -613,7 +614,10 @@ func GetPolicies(statements []Statement, bucketName, prefix string) map[string]B r = r[:len(r)-1] asterisk = "*" } - objectPath := r[len(awsResourcePrefix+bucketName)+1:] + var objectPath string + if len(r) >= len(awsResourcePrefix+bucketName)+1 { + objectPath = r[len(awsResourcePrefix+bucketName)+1:] + } p := GetPolicy(statements, bucketName, objectPath) policyRules[bucketName+"/"+objectPath+asterisk] = p } diff --git a/pkg/policy/bucket-policy_test.go b/pkg/policy/bucket-policy_test.go index b6b4551..1a71d87 100644 --- a/pkg/policy/bucket-policy_test.go +++ b/pkg/policy/bucket-policy_test.go @@ -1592,6 +1592,7 @@ func TestListBucketPolicies(t *testing.T) { downloadUploadCondKeyMap.Add("s3:prefix", set.CreateStringSet("both")) downloadUploadCondMap.Add("StringEquals", downloadUploadCondKeyMap) + commonSetActions := commonBucketActions.Union(readOnlyBucketActions) testCases := []struct { statements []Statement bucketName string @@ -1630,6 +1631,13 @@ func TestListBucketPolicies(t *testing.T) { Principal: User{AWS: set.CreateStringSet("*")}, Resources: set.CreateStringSet("arn:aws:s3:::mybucket/download*"), }}, "mybucket", "", map[string]BucketPolicy{"mybucket/download*": BucketPolicyReadOnly}}, + {[]Statement{ + { + Actions: commonSetActions.Union(readOnlyObjectActions), + Effect: "Allow", + Principal: User{AWS: set.CreateStringSet("*")}, + Resources: set.CreateStringSet("arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"), + }}, "mybucket", "", map[string]BucketPolicy{"mybucket/*": BucketPolicyReadOnly}}, // Write Only {[]Statement{ { diff --git a/pkg/s3utils/utils.go b/pkg/s3utils/utils.go index bfeb73e..adceb7f 100644 --- a/pkg/s3utils/utils.go +++ b/pkg/s3utils/utils.go @@ -143,11 +143,40 @@ func IsAmazonGovCloudEndpoint(endpointURL url.URL) bool { } // IsAmazonFIPSGovCloudEndpoint - Match if it is exactly Amazon S3 FIPS GovCloud endpoint. +// See https://aws.amazon.com/compliance/fips. func IsAmazonFIPSGovCloudEndpoint(endpointURL url.URL) bool { if endpointURL == sentinelURL { return false } - return endpointURL.Host == "s3-fips-us-gov-west-1.amazonaws.com" + return endpointURL.Host == "s3-fips-us-gov-west-1.amazonaws.com" || + endpointURL.Host == "s3-fips.dualstack.us-gov-west-1.amazonaws.com" +} + +// IsAmazonFIPSUSEastWestEndpoint - Match if it is exactly Amazon S3 FIPS US East/West endpoint. +// See https://aws.amazon.com/compliance/fips. +func IsAmazonFIPSUSEastWestEndpoint(endpointURL url.URL) bool { + if endpointURL == sentinelURL { + return false + } + switch endpointURL.Host { + case "s3-fips.us-east-2.amazonaws.com": + case "s3-fips.dualstack.us-west-1.amazonaws.com": + case "s3-fips.dualstack.us-west-2.amazonaws.com": + case "s3-fips.dualstack.us-east-2.amazonaws.com": + case "s3-fips.dualstack.us-east-1.amazonaws.com": + case "s3-fips.us-west-1.amazonaws.com": + case "s3-fips.us-west-2.amazonaws.com": + case "s3-fips.us-east-1.amazonaws.com": + default: + return false + } + return true +} + +// IsAmazonFIPSEndpoint - Match if it is exactly Amazon S3 FIPS endpoint. +// See https://aws.amazon.com/compliance/fips. +func IsAmazonFIPSEndpoint(endpointURL url.URL) bool { + return IsAmazonFIPSUSEastWestEndpoint(endpointURL) || IsAmazonFIPSGovCloudEndpoint(endpointURL) } // IsGoogleEndpoint - Match if it is exactly Google cloud storage endpoint. |