mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-06-13 23:36:45 +03:00
feat(iam): implement ListUserPolicies API action (#8991)
* feat(iam): implement ListUserPolicies API action (#8987) Add ListUserPolicies support to both embedded and standalone IAM servers, resolving the NotImplemented error when calling `aws iam list-user-policies`. * fix: address review feedback for ListUserPolicies - Add handleImplicitUsername for ListUserPolicies in both IAM servers so omitting UserName defaults to the calling user (Gemini review) - Assert synthetic policy name in unit test (CodeRabbit) - Use require.True for error type assertion in integration test (CodeRabbit)
This commit is contained in:
@@ -321,6 +321,13 @@ func TestIAMPolicyManagement(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer iamClient.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(userName)})
|
||||
|
||||
// List user policies before any inline policy is attached
|
||||
listResp, err := iamClient.ListUserPolicies(&iam.ListUserPoliciesInput{
|
||||
UserName: aws.String(userName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, listResp.PolicyNames, "New user should have no inline policies")
|
||||
|
||||
policyName := "test-inline-policy"
|
||||
policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::*"}]}`
|
||||
|
||||
@@ -332,6 +339,14 @@ func TestIAMPolicyManagement(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// List user policies after attaching inline policy
|
||||
listResp, err = iamClient.ListUserPolicies(&iam.ListUserPoliciesInput{
|
||||
UserName: aws.String(userName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, listResp.PolicyNames, "User should have inline policies after PutUserPolicy")
|
||||
assert.False(t, *listResp.IsTruncated)
|
||||
|
||||
// Get user policy
|
||||
getResp, err := iamClient.GetUserPolicy(&iam.GetUserPolicyInput{
|
||||
UserName: aws.String(userName),
|
||||
@@ -348,5 +363,22 @@ func TestIAMPolicyManagement(t *testing.T) {
|
||||
PolicyName: aws.String(policyName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// List user policies after deletion
|
||||
listResp, err = iamClient.ListUserPolicies(&iam.ListUserPoliciesInput{
|
||||
UserName: aws.String(userName),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, listResp.PolicyNames, "User should have no inline policies after DeleteUserPolicy")
|
||||
})
|
||||
|
||||
t.Run("list_user_policies_nonexistent_user", func(t *testing.T) {
|
||||
_, err := iamClient.ListUserPolicies(&iam.ListUserPoliciesInput{
|
||||
UserName: aws.String("nonexistent-user-for-list-policies"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
awsErr, ok := err.(awserr.Error)
|
||||
require.True(t, ok, "Expected AWS error type")
|
||||
assert.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,6 +178,17 @@ type ListAttachedUserPoliciesResponse struct {
|
||||
CommonResponse
|
||||
}
|
||||
|
||||
// ListUserPoliciesResponse is the response for ListUserPolicies action.
|
||||
type ListUserPoliciesResponse struct {
|
||||
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListUserPoliciesResponse"`
|
||||
ListUserPoliciesResult struct {
|
||||
PolicyNames []string `xml:"PolicyNames>member"`
|
||||
IsTruncated bool `xml:"IsTruncated"`
|
||||
Marker string `xml:"Marker,omitempty"`
|
||||
} `xml:"ListUserPoliciesResult"`
|
||||
CommonResponse
|
||||
}
|
||||
|
||||
// GetUserPolicyResponse is the response for GetUserPolicy action.
|
||||
type GetUserPolicyResponse struct {
|
||||
XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserPolicyResponse"`
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -529,6 +530,40 @@ func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, val
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListUserPolicies lists the names of inline policies attached to a user.
|
||||
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUserPolicies.html
|
||||
func (iama *IamApiServer) ListUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListUserPoliciesResponse, iamError *IamError) {
|
||||
resp = &ListUserPoliciesResponse{}
|
||||
userName := values.Get("UserName")
|
||||
|
||||
// Verify the user exists
|
||||
found := false
|
||||
for _, ident := range s3cfg.Identities {
|
||||
if ident.Name == userName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
|
||||
}
|
||||
|
||||
// List inline policy names from persistent storage
|
||||
policies := Policies{}
|
||||
if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
|
||||
if userPolicies := policies.InlinePolicies[userName]; userPolicies != nil {
|
||||
for policyName := range userPolicies {
|
||||
resp.ListUserPoliciesResult.PolicyNames = append(resp.ListUserPoliciesResult.PolicyNames, policyName)
|
||||
}
|
||||
sort.Strings(resp.ListUserPoliciesResult.PolicyNames)
|
||||
}
|
||||
resp.ListUserPoliciesResult.IsTruncated = false
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetPolicy retrieves a managed policy by ARN.
|
||||
func (iama *IamApiServer) GetPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetPolicyResponse, iamError *IamError) {
|
||||
resp = &GetPolicyResponse{}
|
||||
@@ -1062,6 +1097,15 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
|
||||
writeIamErrorResponse(w, r, reqID, err)
|
||||
return
|
||||
}
|
||||
case "ListUserPolicies":
|
||||
iama.handleImplicitUsername(r, values)
|
||||
var err *IamError
|
||||
response, err = iama.ListUserPolicies(s3cfg, values)
|
||||
if err != nil {
|
||||
writeIamErrorResponse(w, r, reqID, err)
|
||||
return
|
||||
}
|
||||
changed = false
|
||||
case "GetPolicy":
|
||||
var err *IamError
|
||||
response, err = iama.GetPolicy(s3cfg, values)
|
||||
|
||||
@@ -23,6 +23,7 @@ type (
|
||||
PutUserPolicyResponse = iamlib.PutUserPolicyResponse
|
||||
DeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse
|
||||
GetUserPolicyResponse = iamlib.GetUserPolicyResponse
|
||||
ListUserPoliciesResponse = iamlib.ListUserPoliciesResponse
|
||||
GetPolicyResponse = iamlib.GetPolicyResponse
|
||||
DeletePolicyResponse = iamlib.DeletePolicyResponse
|
||||
ListPoliciesResponse = iamlib.ListPoliciesResponse
|
||||
|
||||
@@ -99,6 +99,7 @@ type (
|
||||
iamPutUserPolicyResponse = iamlib.PutUserPolicyResponse
|
||||
iamDeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse
|
||||
iamGetUserPolicyResponse = iamlib.GetUserPolicyResponse
|
||||
iamListUserPoliciesResponse = iamlib.ListUserPoliciesResponse
|
||||
iamAttachUserPolicyResponse = iamlib.AttachUserPolicyResponse
|
||||
iamDetachUserPolicyResponse = iamlib.DetachUserPolicyResponse
|
||||
iamListAttachedUserPoliciesResponse = iamlib.ListAttachedUserPoliciesResponse
|
||||
@@ -939,6 +940,23 @@ func (e *EmbeddedIamApi) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, valu
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
||||
}
|
||||
|
||||
// ListUserPolicies lists the names of inline policies attached to a user.
|
||||
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUserPolicies.html
|
||||
func (e *EmbeddedIamApi) ListUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListUserPoliciesResponse, *iamError) {
|
||||
resp := &iamListUserPoliciesResponse{}
|
||||
userName := values.Get("UserName")
|
||||
for _, ident := range s3cfg.Identities {
|
||||
if ident.Name == userName {
|
||||
if len(ident.Actions) > 0 {
|
||||
resp.ListUserPoliciesResult.PolicyNames = []string{userName + "_policy"}
|
||||
}
|
||||
resp.ListUserPoliciesResult.IsTruncated = false
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user.
|
||||
func (e *EmbeddedIamApi) AttachUserPolicy(ctx context.Context, values url.Values) (*iamAttachUserPolicyResponse, *iamError) {
|
||||
resp := &iamAttachUserPolicyResponse{}
|
||||
@@ -1906,7 +1924,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
|
||||
action := values.Get("Action")
|
||||
if e.readOnly {
|
||||
switch action {
|
||||
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListPolicyVersions", "GetPolicyVersion", "ListServiceAccounts", "GetServiceAccount",
|
||||
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListUserPolicies", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListPolicyVersions", "GetPolicyVersion", "ListServiceAccounts", "GetServiceAccount",
|
||||
"GetGroup", "ListGroups", "ListAttachedGroupPolicies", "ListGroupsForUser":
|
||||
// Allowed read-only actions
|
||||
default:
|
||||
@@ -2003,6 +2021,13 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
|
||||
if iamErr != nil {
|
||||
return nil, iamErr
|
||||
}
|
||||
case "ListUserPolicies":
|
||||
var iamErr *iamError
|
||||
response, iamErr = e.ListUserPolicies(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
return nil, iamErr
|
||||
}
|
||||
changed = false
|
||||
case "AttachUserPolicy":
|
||||
var iamErr *iamError
|
||||
response, iamErr = e.AttachUserPolicy(ctx, values)
|
||||
@@ -2198,7 +2223,7 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Handle implicit username for HTTP requests
|
||||
switch r.Form.Get("Action") {
|
||||
case "ListAccessKeys", "CreateAccessKey", "DeleteAccessKey", "UpdateAccessKey":
|
||||
case "ListAccessKeys", "CreateAccessKey", "DeleteAccessKey", "UpdateAccessKey", "ListUserPolicies":
|
||||
e.handleImplicitUsername(r, values)
|
||||
case "CreateServiceAccount":
|
||||
createdBy := s3_constants.GetIdentityNameFromContext(r)
|
||||
|
||||
@@ -461,6 +461,79 @@ func TestEmbeddedIamDeleteUserPolicyUserNotFound(t *testing.T) {
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamListUserPolicies tests listing inline policies for a user.
|
||||
func TestEmbeddedIamListUserPolicies(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{
|
||||
Name: "UserWithPolicy",
|
||||
Actions: []string{"Read", "Write"},
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UserWithoutPolicy",
|
||||
Credentials: []*iam_pb.Credential{
|
||||
{AccessKey: UserAccessKeyPrefix + "TEST67890", SecretKey: "secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// List policies for user with actions
|
||||
form := url.Values{}
|
||||
form.Set("Action", "ListUserPolicies")
|
||||
form.Set("UserName", "UserWithPolicy")
|
||||
|
||||
req, _ := http.NewRequest("POST", "/", nil)
|
||||
req.PostForm = form
|
||||
req.Form = form
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
apiRouter := mux.NewRouter().SkipClean(true)
|
||||
apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
|
||||
apiRouter.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "ListUserPoliciesResponse")
|
||||
assert.Contains(t, rr.Body.String(), "PolicyNames")
|
||||
assert.Contains(t, rr.Body.String(), "UserWithPolicy_policy")
|
||||
|
||||
// List policies for user without actions
|
||||
form2 := url.Values{}
|
||||
form2.Set("Action", "ListUserPolicies")
|
||||
form2.Set("UserName", "UserWithoutPolicy")
|
||||
|
||||
req2, _ := http.NewRequest("POST", "/", nil)
|
||||
req2.PostForm = form2
|
||||
req2.Form = form2
|
||||
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
rr2 := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(rr2, req2)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr2.Code)
|
||||
assert.Contains(t, rr2.Body.String(), "ListUserPoliciesResponse")
|
||||
|
||||
// List policies for nonexistent user
|
||||
form3 := url.Values{}
|
||||
form3.Set("Action", "ListUserPolicies")
|
||||
form3.Set("UserName", "NonExistentUser")
|
||||
|
||||
req3, _ := http.NewRequest("POST", "/", nil)
|
||||
req3.PostForm = form3
|
||||
req3.Form = form3
|
||||
req3.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
rr3 := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(rr3, req3)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, rr3.Code)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamAttachUserPolicy tests attaching a managed policy to a user.
|
||||
func TestEmbeddedIamAttachUserPolicy(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
|
||||
Reference in New Issue
Block a user