fix(iam): return a valid user ARN from CreateUser and GetUser (#9794)

* fix(iam): return a valid user ARN from CreateUser and GetUser

The terraform aws provider 6.41 reads a user back after creating it and
blocks until GetUser returns a value that passes arn.IsARN. We only set
UserName, so the ARN was empty and apply hung until the 2m timeout.
Populate Arn (and Path) via a shared iam.NewUser helper in both the
embedded and standalone IAM handlers.

* fix(iam): use the userName parameter directly in NewUser

Drop the redundant local copy; the value parameter is already function-local.

* fix(iam): return full user objects with ARNs from GetGroup

GetGroup listed members with only UserName set. Build them via the shared
NewUser helper so group members carry a valid Arn and Path like the other
user responses, in both the embedded and standalone IAM handlers.
This commit is contained in:
Chris Lu 2026-06-02 22:01:57 -07:00 committed by GitHub
parent f711868fb6
commit 4e5839ce82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 62 additions and 10 deletions

View File

@ -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."

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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{

View File

@ -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
}

View File

@ -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