Files
seaweedfs/weed/s3api/auth_credentials_policy_document.go
T
Chris LuandGitHub 0002e5cc7f s3api: load document-style policies from the advanced IAM config (#10481)
* s3api: load document-style policies from the advanced IAM config

The advanced IAM file doubles as the S3 identity config when only
-s3.iam.config is given. protojson drops its "document" field, so every
policy landed with empty content and warned "skipping invalid policy" on
each reload. Worse, if the same file also declares identities the empty
content sticks in the policy map and fails the whole runtime policy sync
into the IAM manager, so policies created later never reach it.

* iam: skip an unparsable policy instead of failing the whole runtime sync

One policy the engine cannot parse aborted SyncRuntimePolicies before it
touched anything, so every other policy stayed unsynced and the engine
kept serving whatever it last held.

* s3api: reject a non-role RoleArn in AssumeRole as a bad request

arn:aws:iam:::user/name can never resolve to a role, but the handler ran
it through the trust-policy check and answered "not authorized to assume
role", pointing the caller at a permission problem they do not have.

* s3api: build the policy content before touching the entry

Deleting "document" up front meant a marshal failure left the policy with
neither field, so a later rewrite would emit it with no definition at all.

* iam: pin the fail-closed handling of an unparsable policy

Say in the comment that dropping it from the desired set deletes it from
the engine on purpose, and cover it with a test.

* s3api: widen the non-role RoleArn test to canonical ARN shapes

The reported ARN omits the account id; a user ARN that carries one, and a
non-principal ARN, must be rejected the same way.
2026-07-29 10:30:30 -07:00

85 lines
2.3 KiB
Go

package s3api
import (
"encoding/json"
"strings"
)
// normalizeAdvancedIAMPolicies rewrites policies written in the advanced IAM
// form ({"name": ..., "document": {...}}) into the S3 config form
// ({"name": ..., "content": "{...}"}).
//
// The advanced IAM file given by -s3.iam.config is also parsed as the S3
// identity config when no -s3.config is given, and protojson drops the unknown
// "document" field. That leaves a policy with empty content, which fails every
// later parse and takes the whole runtime policy sync down with it.
func normalizeAdvancedIAMPolicies(configContent []byte) []byte {
var root map[string]json.RawMessage
if err := json.Unmarshal(configContent, &root); err != nil {
return configContent
}
rawPolicies, found := root["policies"]
if !found {
return configContent
}
var policies []map[string]json.RawMessage
if err := json.Unmarshal(rawPolicies, &policies); err != nil {
return configContent
}
rewritten := false
for _, policy := range policies {
document, hasDocument := policy["document"]
if !hasDocument || hasPolicyContent(policy) {
continue
}
// A document already written as a JSON string is the content verbatim;
// an inline object becomes the JSON encoding of its own bytes. Nothing
// is mutated until the content is in hand, so a failure leaves the
// policy as it was rather than stripping its only definition.
content := document
if !isJSONString(document) {
encoded, err := json.Marshal(string(document))
if err != nil {
continue
}
content = encoded
}
delete(policy, "document")
policy["content"] = content
rewritten = true
}
if !rewritten {
return configContent
}
encodedPolicies, err := json.Marshal(policies)
if err != nil {
return configContent
}
root["policies"] = encodedPolicies
normalized, err := json.Marshal(root)
if err != nil {
return configContent
}
return normalized
}
func hasPolicyContent(policy map[string]json.RawMessage) bool {
raw, found := policy["content"]
if !found {
return false
}
var content string
if err := json.Unmarshal(raw, &content); err != nil {
// Not a string: leave whatever it is for the proto parser to reject.
return true
}
return strings.TrimSpace(content) != ""
}
func isJSONString(raw json.RawMessage) bool {
var s string
return json.Unmarshal(raw, &s) == nil
}