iam: take the implicit user name from the verified identity (#11903)

* iam: take the implicit user name from the verified identity

For self-service actions sent without UserName, the implicit user was
resolved by re-parsing the raw Authorization header for a SigV4
credential and looking that access key up. The header is not what
authentication verified: a request can be authenticated by one
mechanism while carrying a header that names another user's access
key, and the action then ran against that other user.

The auth middleware already records the verified identity in the
request context, so use it, on both the standalone iam server and the
embedded IAM surface. When no verified identity is present the name is
left unset rather than guessed.

* iam: test implicit user name resolution

Cover a request authenticated by presigned credentials that also
carries a junk SigV4 header naming another user: the self-service
actions must run as the verified signer. Update the unit tests for the
context-based lookup.

* iam: resolve the implicit user for standalone GetUser too

The embedded surface fills UserName for GetUser; the standalone server
skipped it, so an implicit GetUser on `weed iam` answered NoSuchEntity.
Same verified-identity source as the other self-service actions.

* iam: cover all implicit-username actions in the regression test
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-21 12:21:04 -07:00
1 parent 593cfee85b
commit bb1ba8b247
5 files changed
+123 -149

No files matched your search

+72
View File
@@ -2,10 +2,16 @@ package iamapi
import (
"context"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
@@ -141,3 +147,69 @@ func TestIamManagementAllowedForAdmin(t *testing.T) {
"", "Action=CreateUser&UserName=new-user&Version=2010-05-08")
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
// The implicit UserName must come from the identity authentication verified,
// never from re-parsing the Authorization header: a request can carry presigned
// credentials for one user alongside a junk SigV4 header naming another.
func TestImplicitUserNameUsesVerifiedIdentity(t *testing.T) {
server := newAuthzTestServer(t)
expires := strconv.FormatInt(time.Now().Add(10*time.Minute).Unix(), 10)
sts := "POST\n\napplication/x-www-form-urlencoded\n" + expires + "\n/"
mac := hmac.New(sha1.New, []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"))
mac.Write([]byte(sts))
sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
q := url.Values{"AWSAccessKeyId": {"AKIAIOSFODNN7EXAMPLE"}, "Expires": {expires}, "Signature": {sig}}
post := func(t *testing.T, body string) (int, string) {
t.Helper()
req, err := http.NewRequest(http.MethodPost, server.URL+"/?"+q.Encode(), strings.NewReader(body))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=AKIATESTFAKEADMIN0001/20261021/us-east-1/iam/aws4_request, SignedHeaders=host, Signature=00")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
t.Logf("%d %s", resp.StatusCode, b)
return resp.StatusCode, string(b)
}
for _, action := range []string{"ListAccessKeys", "CreateAccessKey", "GetUser"} {
t.Run(action, func(t *testing.T) {
code, body := post(t, "Action="+action+"&Version=2010-05-08")
assert.Equal(t, http.StatusOK, code)
assert.NotContains(t, body, "iam_admin")
assert.Contains(t, body, "power_user")
})
}
// ListUserPolicies is not self-service: a caller without IAM rights is
// denied whichever name the implicit resolution lands on.
t.Run("ListUserPolicies", func(t *testing.T) {
code, body := post(t, "Action=ListUserPolicies&Version=2010-05-08")
assert.Equal(t, http.StatusForbidden, code)
assert.NotContains(t, body, "iam_admin")
})
// A write aimed at the header-named admin's key resolves to the verified
// caller instead: the key is not power_user's, so it is not found.
t.Run("UpdateAccessKey", func(t *testing.T) {
code, body := post(t, "Action=UpdateAccessKey&AccessKeyId=AKIATESTFAKEADMIN0001&Status=Inactive&Version=2010-05-08")
assert.Equal(t, http.StatusNotFound, code)
assert.Contains(t, body, "NoSuchEntity")
})
t.Run("DeleteAccessKey", func(t *testing.T) {
code, _ := post(t, "Action=DeleteAccessKey&AccessKeyId=AKIATESTFAKEADMIN0001&Version=2010-05-08")
assert.Equal(t, http.StatusOK, code)
})
// The admin key the junk header named is untouched: still present and active.
resp := postSignedIamAction(t, server.URL, "AKIATESTFAKEADMIN0001", "testAdminSecretFake0000000000000000000000",
"", "Action=ListAccessKeys&Version=2010-05-08")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Contains(t, string(body), "AKIATESTFAKEADMIN0001")
assert.Contains(t, string(body), "<Status>Active</Status>")
}