From 638f6ff4334b6912d27e7ddb1c3def2f9559b797 Mon Sep 17 00:00:00 2001 From: Minsoo Kim Date: Fri, 19 Jun 2026 14:20:56 +0900 Subject: [PATCH] admin: surface user inline policies in object store user details (#10013) GetObjectStoreUserDetails only returned identity.PolicyNames (attached managed policies) and omitted per-user inline policies. Inline policies are stored separately from the identity record and are authoritative at S3 enforcement time (they take precedence over the legacy Actions list), so an operator could not see what actually governed a user's access via the admin API/UI. Include inline policy names (via credentialManager.ListUserInlinePolicies) in the returned PolicyNames. Adds a unit test using the memory credential store. --- weed/admin/dash/user_inline_policy_test.go | 62 ++++++++++++++++++++++ weed/admin/dash/user_management.go | 12 +++++ 2 files changed, 74 insertions(+) create mode 100644 weed/admin/dash/user_inline_policy_test.go diff --git a/weed/admin/dash/user_inline_policy_test.go b/weed/admin/dash/user_inline_policy_test.go new file mode 100644 index 000000000..93db7125a --- /dev/null +++ b/weed/admin/dash/user_inline_policy_test.go @@ -0,0 +1,62 @@ +package dash + +import ( + "context" + "testing" + + "github.com/seaweedfs/seaweedfs/weed/credential" + _ "github.com/seaweedfs/seaweedfs/weed/credential/memory" // register the memory credential store + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" +) + +// TestGetObjectStoreUserDetailsIncludesInlinePolicies verifies that inline user +// policies — which are stored separately from the identity record and are +// authoritative at S3 enforcement time (they take precedence over the legacy +// Actions list) — are surfaced in the admin user details. They were previously +// invisible in the admin API/UI, so an operator could not tell what actually +// governed a user's access. +func TestGetObjectStoreUserDetailsIncludesInlinePolicies(t *testing.T) { + cm, err := credential.NewCredentialManagerWithDefaults(credential.StoreTypeMemory) + if err != nil { + t.Fatalf("failed to init credential manager: %v", err) + } + ctx := context.Background() + + const username = "svc-app" + if err := cm.CreateUser(ctx, &iam_pb.Identity{Name: username}); err != nil { + t.Fatalf("failed to create user: %v", err) + } + + const inlinePolicyName = "svc-app-policy" + doc := policy_engine.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy_engine.PolicyStatement{ + { + Effect: "Allow", + Action: policy_engine.NewStringOrStringSlice("s3:GetObject"), + Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::ml-artifacts/*"), + }, + }, + } + if err := cm.PutUserInlinePolicy(ctx, username, inlinePolicyName, doc); err != nil { + t.Fatalf("failed to put inline policy: %v", err) + } + + s := &AdminServer{credentialManager: cm} + details, err := s.GetObjectStoreUserDetails(username) + if err != nil { + t.Fatalf("GetObjectStoreUserDetails failed: %v", err) + } + + found := false + for _, p := range details.PolicyNames { + if p == inlinePolicyName { + found = true + break + } + } + if !found { + t.Errorf("expected inline policy %q in details.PolicyNames, got %v", inlinePolicyName, details.PolicyNames) + } +} diff --git a/weed/admin/dash/user_management.go b/weed/admin/dash/user_management.go index d1b599acc..c7ed5baf7 100644 --- a/weed/admin/dash/user_management.go +++ b/weed/admin/dash/user_management.go @@ -9,6 +9,7 @@ import ( "time" "github.com/seaweedfs/seaweedfs/weed/credential" + "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/iam" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" ) @@ -190,6 +191,17 @@ func (s *AdminServer) GetObjectStoreUserDetails(username string) (*UserDetails, PolicyNames: identity.PolicyNames, } + // Inline user policies are stored separately from the identity record and + // are authoritative at enforcement time (they take precedence over the + // legacy Actions list). Surface their names alongside attached policies so + // the admin reflects the user's actual permissions rather than only the + // (potentially overridden) Actions. + if inlinePolicyNames, err := s.credentialManager.ListUserInlinePolicies(ctx, username); err != nil { + glog.Warningf("failed to list inline policies for user %s: %v", username, err) + } else if len(inlinePolicyNames) > 0 { + details.PolicyNames = append(details.PolicyNames, inlinePolicyNames...) + } + // Set email from account if available if identity.Account != nil { details.Email = identity.Account.EmailAddress