diff --git a/weed/iam/constants.go b/weed/iam/constants.go index 974645be7..9f48e901d 100644 --- a/weed/iam/constants.go +++ b/weed/iam/constants.go @@ -9,6 +9,10 @@ const ( // Policy document version const PolicyDocumentVersion = "2012-10-17" +// DefaultAccountID is the placeholder AWS account id used in generated ARNs, +// matching the value used throughout the S3 API. +const DefaultAccountID = "000000000000" + // Error message templates const UserDoesNotExist = "the user with name %s cannot be found." diff --git a/weed/iam/helpers.go b/weed/iam/helpers.go index cbbc86fb2..ae8c8fd6a 100644 --- a/weed/iam/helpers.go +++ b/weed/iam/helpers.go @@ -7,6 +7,7 @@ import ( "math/big" "sort" + "github.com/aws/aws-sdk-go/service/iam" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" ) @@ -17,6 +18,21 @@ func Hash(s *string) string { return fmt.Sprintf("%x", h.Sum(nil)) } +// UserArn builds an AWS-compatible IAM user ARN. +func UserArn(userName string) string { + return fmt.Sprintf("arn:aws:iam::%s:user/%s", DefaultAccountID, userName) +} + +// NewUser builds an iam.User for IAM API responses. The Arn must be a real ARN: +// the terraform aws provider (>= 6.41) reads a user back after creating it and +// blocks until GetUser returns a value that passes arn.IsARN, so an empty Arn +// leaves apply hanging until it times out. +func NewUser(userName string) iam.User { + arn := UserArn(userName) + path := "/" + return iam.User{UserName: &userName, Arn: &arn, Path: &path} +} + // GenerateRandomString generates a cryptographically secure random string. // Uses crypto/rand for security-sensitive credential generation. func GenerateRandomString(length int, charset string) (string, error) { diff --git a/weed/iamapi/iamapi_group_handlers.go b/weed/iamapi/iamapi_group_handlers.go index 3e44294d2..8edcb296b 100644 --- a/weed/iamapi/iamapi_group_handlers.go +++ b/weed/iamapi/iamapi_group_handlers.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/service/iam" "github.com/seaweedfs/seaweedfs/weed/glog" + iamlib "github.com/seaweedfs/seaweedfs/weed/iam" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" @@ -105,8 +106,8 @@ func (iama *IamApiServer) GetGroup(s3cfg *iam_pb.S3ApiConfiguration, values url. if g.Name == groupName { resp.GetGroupResult.Group.GroupName = &g.Name for _, member := range g.Members { - m := member - resp.GetGroupResult.Users = append(resp.GetGroupResult.Users, &iam.User{UserName: &m}) + user := iamlib.NewUser(member) + resp.GetGroupResult.Users = append(resp.GetGroupResult.Users, &user) } return resp, nil } diff --git a/weed/iamapi/iamapi_management_handlers.go b/weed/iamapi/iamapi_management_handlers.go index 1248b7e74..f81a8d76c 100644 --- a/weed/iamapi/iamapi_management_handlers.go +++ b/weed/iamapi/iamapi_management_handlers.go @@ -188,7 +188,8 @@ func validateAccessKeyStatus(status string) error { func (iama *IamApiServer) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListUsersResponse) { resp = &ListUsersResponse{} for _, ident := range s3cfg.Identities { - resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &iam.User{UserName: &ident.Name}) + user := iamlib.NewUser(ident.Name) + resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &user) } return resp } @@ -218,7 +219,7 @@ func (iama *IamApiServer) ListAccessKeys(s3cfg *iam_pb.S3ApiConfiguration, value func (iama *IamApiServer) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreateUserResponse) { resp = &CreateUserResponse{} userName := values.Get("UserName") - resp.CreateUserResult.User.UserName = &userName + resp.CreateUserResult.User = iamlib.NewUser(userName) s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName}) return resp } @@ -253,7 +254,7 @@ func (iama *IamApiServer) GetUser(s3cfg *iam_pb.S3ApiConfiguration, userName str resp = &GetUserResponse{} for _, ident := range s3cfg.Identities { if userName == ident.Name { - resp.GetUserResult.User = iam.User{UserName: &ident.Name} + resp.GetUserResult.User = iamlib.NewUser(ident.Name) return resp, nil } } diff --git a/weed/iamapi/iamapi_management_handlers_test.go b/weed/iamapi/iamapi_management_handlers_test.go index 9bffd4f85..3a85149fd 100644 --- a/weed/iamapi/iamapi_management_handlers_test.go +++ b/weed/iamapi/iamapi_management_handlers_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/iam" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" @@ -631,6 +633,23 @@ func TestListAttachedUserPolicies(t *testing.T) { assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code) } +// TestUserArnIsValid is a regression test for issue #9786: the terraform aws +// provider (>= 6.41) reads a user back after creating it and blocks until +// GetUser returns a value that passes arn.IsARN. +func TestUserArnIsValid(t *testing.T) { + iama := newTestIamApiServer(Policies{}) + s3cfg := &iam_pb.S3ApiConfiguration{} + + createResp := iama.CreateUser(s3cfg, url.Values{"UserName": []string{"alice"}}) + assert.True(t, arn.IsARN(aws.StringValue(createResp.CreateUserResult.User.Arn)), + "CreateUser must return a valid ARN, got %q", aws.StringValue(createResp.CreateUserResult.User.Arn)) + + getResp, iamErr := iama.GetUser(s3cfg, "alice") + assert.Nil(t, iamErr) + assert.True(t, arn.IsARN(aws.StringValue(getResp.GetUserResult.User.Arn)), + "GetUser must return a valid ARN, got %q", aws.StringValue(getResp.GetUserResult.User.Arn)) +} + func TestCreateAccessKeyWithCallerSuppliedKeys(t *testing.T) { iama := newTestIamApiServer(Policies{}) s3cfg := &iam_pb.S3ApiConfiguration{ diff --git a/weed/s3api/s3api_embedded_iam.go b/weed/s3api/s3api_embedded_iam.go index 4078fdf60..751a339ca 100644 --- a/weed/s3api/s3api_embedded_iam.go +++ b/weed/s3api/s3api_embedded_iam.go @@ -267,7 +267,8 @@ func (e *EmbeddedIamApi) ReloadConfiguration() error { func (e *EmbeddedIamApi) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) *iamListUsersResponse { resp := &iamListUsersResponse{} for _, ident := range s3cfg.Identities { - resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &iam.User{UserName: &ident.Name}) + user := iamlib.NewUser(ident.Name) + resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &user) } return resp } @@ -315,7 +316,7 @@ func (e *EmbeddedIamApi) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url } } - resp.CreateUserResult.User.UserName = &userName + resp.CreateUserResult.User = iamlib.NewUser(userName) s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName}) // Disabled defaults to false (enabled) return resp, nil } @@ -355,7 +356,7 @@ func (e *EmbeddedIamApi) GetUser(s3cfg *iam_pb.S3ApiConfiguration, userName stri resp := &iamGetUserResponse{} for _, ident := range s3cfg.Identities { if userName == ident.Name { - resp.GetUserResult.User = iam.User{UserName: &ident.Name} + resp.GetUserResult.User = iamlib.NewUser(ident.Name) return resp, nil } } @@ -2096,8 +2097,8 @@ func (e *EmbeddedIamApi) GetGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.V if g.Name == groupName { resp.GetGroupResult.Group.GroupName = &g.Name for _, member := range g.Members { - memberName := member - resp.GetGroupResult.Users = append(resp.GetGroupResult.Users, &iam.User{UserName: &memberName}) + user := iamlib.NewUser(member) + resp.GetGroupResult.Users = append(resp.GetGroupResult.Users, &user) } return resp, nil } diff --git a/weed/s3api/s3api_embedded_iam_test.go b/weed/s3api/s3api_embedded_iam_test.go index 67d709f8d..6701e5e17 100644 --- a/weed/s3api/s3api_embedded_iam_test.go +++ b/weed/s3api/s3api_embedded_iam_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" "github.com/gorilla/mux" @@ -189,6 +190,10 @@ func TestEmbeddedIamCreateUser(t *testing.T) { assert.NotNil(t, out.CreateUserResult.User.UserName) assert.Equal(t, "TestUser", *out.CreateUserResult.User.UserName) + // Issue #9786: terraform aws provider >= 6.41 blocks on a valid ARN. + assert.True(t, arn.IsARN(aws.StringValue(out.CreateUserResult.User.Arn)), + "CreateUser must return a valid ARN, got %q", aws.StringValue(out.CreateUserResult.User.Arn)) + // Verify user was persisted in config assert.Len(t, api.mockConfig.Identities, 1) assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name) @@ -337,6 +342,11 @@ func TestEmbeddedIamGetUser(t *testing.T) { // Verify response contains correct username assert.NotNil(t, out.GetUserResult.User.UserName) assert.Equal(t, "TestUser", *out.GetUserResult.User.UserName) + + // Issue #9786: the terraform aws provider reads the user back after creating + // it and waits until GetUser returns a value that passes arn.IsARN. + assert.True(t, arn.IsARN(aws.StringValue(out.GetUserResult.User.Arn)), + "GetUser must return a valid ARN, got %q", aws.StringValue(out.GetUserResult.User.Arn)) } // TestEmbeddedIamGetUserImplicitUsername verifies GetUser without a UserName defaults