mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-18 12:08:32 +02:00
s3api: support group inline policies + Condition enforcement (#9569)
* test(s3api): cover IAM inline policy aws:SourceIp + group inline gap Unit tests under weed/s3api/ drive PutUserPolicy / PutGroupPolicy → reload → VerifyActionPermission with a synthetic 127.0.0.1 request and assert that the policy's IpAddress condition flips the outcome. The user-policy cases pass on master (hydrateRuntimePolicies already routes inline docs through the policy engine, so Condition blocks are honored end- to-end). The group-policy case fails: PutGroupPolicy still returns NotImplemented, so a group inline doc never lands in the engine. Integration counterparts live under test/s3/iam/ and exercise the same paths against a live SeaweedFS S3+IAM endpoint. * s3api: support group inline policies + Condition enforcement PutGroupPolicy/GetGroupPolicy/DeleteGroupPolicy/ListGroupPolicies used to return NotImplemented in embedded IAM mode, so anything attached to a group as an inline doc — including aws:SourceIp or any other Condition — was simply unreachable. Wire the four endpoints to the credential-store methods that were already in place (memory, postgres, filer_etc all implement GroupInlinePolicyStore). On every config reload, hydrateRuntimePolicies now also walks LoadGroupInlinePolicies, registers each doc in the IAM policy engine under __inline_group_policy__/<group>/<policy>, and appends that key to Group.PolicyNames so evaluateIAMPolicies picks it up through its existing group walk. PutGroupPolicy/DeleteGroupPolicy are added to the ReloadConfiguration trigger list in DoActions. Side fix: MemoryStore.LoadConfiguration now surfaces store.groups too. Without it iam.groups never repopulated on a memory-store reload, so group policy evaluation silently no-op'd whether the policy was inline or attached. The existing tests didn't notice because no test reloaded through cm after creating a group. The NotImplemented unit test is inverted to drive the new round-trip. * s3api: drop redundant refreshIAMConfiguration from Put/DeleteGroupPolicy DoActions already triggers ReloadConfiguration for both actions via the explicit reload list, so calling refreshIAMConfiguration inline runs the load twice per request. Per PR review. * s3api: scope group-policy resource names per test; tighten deny polling - Integration test resource names get a per-test suffix so retried or parallel CI jobs don't trip EntityAlreadyExists / BucketAlreadyExists. - Deny-path Eventually loops gate on AccessDenied via a typed helper rather than any non-nil error; transient setup errors no longer end the wait prematurely. - ListGroupPolicies returns ServiceFailure when the credential manager is nil, matching Put/Get/DeleteGroupPolicy. * test(s3 iam): cover both IPv4 and IPv6 loopback in allow CIDRs CI runners with happy-eyeballs resolve `localhost` to ::1 first, in which case a 127.0.0.0/8-only allow would silently never match and the deny-driven enforcement test would hang for the allow case. Add ::1/128 to every loopback-matching policy so the allow path works regardless of which loopback family the SDK lands on.
This commit is contained in:
1 parent
77ac781bbd
commit
285025eb73
6 files changed
+809
-61
No files matched your search
@@ -1986,29 +1986,130 @@ func (e *EmbeddedIamApi) ListGroupsForUser(s3cfg *iam_pb.S3ApiConfiguration, val
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// notImplementedError returns a NotImplemented IAM error for the embedded server.
|
||||
func notImplementedGroupInlineError() *iamError {
|
||||
return &iamError{Code: s3err.GetAPIError(s3err.ErrNotImplemented).Code, Error: fmt.Errorf("group inline policies are not supported in embedded IAM mode; use the standalone IAM server or managed policies (AttachGroupPolicy)")}
|
||||
// groupExists returns true if a group of the given name is present in the
|
||||
// supplied S3 IAM configuration.
|
||||
func groupExists(s3cfg *iam_pb.S3ApiConfiguration, groupName string) bool {
|
||||
for _, g := range s3cfg.Groups {
|
||||
if g.Name == groupName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PutGroupPolicy is not supported in embedded IAM mode.
|
||||
// PutGroupPolicy attaches an inline policy to a group. The document (including
|
||||
// any Condition block) is persisted via the credential store and registered
|
||||
// in the IAM policy engine via hydrateRuntimePolicies on the next reload.
|
||||
func (e *EmbeddedIamApi) PutGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamPutGroupPolicyResponse, *iamError) {
|
||||
return &iamPutGroupPolicyResponse{}, notImplementedGroupInlineError()
|
||||
resp := &iamPutGroupPolicyResponse{}
|
||||
groupName := values.Get("GroupName")
|
||||
if groupName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
|
||||
}
|
||||
policyName := values.Get("PolicyName")
|
||||
if policyName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
|
||||
}
|
||||
if e.credentialManager == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
|
||||
}
|
||||
policyDocumentString := values.Get("PolicyDocument")
|
||||
policyDocument, err := e.GetPolicyDocument(&policyDocumentString)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err}
|
||||
}
|
||||
if !groupExists(s3cfg, groupName) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
|
||||
}
|
||||
if err := e.credentialManager.PutGroupInlinePolicy(context.Background(), groupName, policyName, policyDocument); err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
// DoActions reloads the IAM configuration for PutGroupPolicy, so the
|
||||
// engine registration via hydrateRuntimePolicies happens there. No
|
||||
// internal refresh is needed.
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetGroupPolicy is not supported in embedded IAM mode.
|
||||
// GetGroupPolicy returns the JSON document for an inline policy on a group.
|
||||
func (e *EmbeddedIamApi) GetGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamGetGroupPolicyResponse, *iamError) {
|
||||
return &iamGetGroupPolicyResponse{}, notImplementedGroupInlineError()
|
||||
resp := &iamGetGroupPolicyResponse{}
|
||||
groupName := values.Get("GroupName")
|
||||
policyName := values.Get("PolicyName")
|
||||
if groupName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
|
||||
}
|
||||
if policyName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
|
||||
}
|
||||
if !groupExists(s3cfg, groupName) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
|
||||
}
|
||||
if e.credentialManager == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
|
||||
}
|
||||
doc, err := e.credentialManager.GetGroupInlinePolicy(context.Background(), groupName, policyName)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
if doc == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found on group %s", policyName, groupName)}
|
||||
}
|
||||
docJSON, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
resp.GetGroupPolicyResult.GroupName = groupName
|
||||
resp.GetGroupPolicyResult.PolicyName = policyName
|
||||
resp.GetGroupPolicyResult.PolicyDocument = string(docJSON)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteGroupPolicy is not supported in embedded IAM mode.
|
||||
// DeleteGroupPolicy removes an inline policy from a group.
|
||||
func (e *EmbeddedIamApi) DeleteGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamDeleteGroupPolicyResponse, *iamError) {
|
||||
return &iamDeleteGroupPolicyResponse{}, notImplementedGroupInlineError()
|
||||
resp := &iamDeleteGroupPolicyResponse{}
|
||||
groupName := values.Get("GroupName")
|
||||
policyName := values.Get("PolicyName")
|
||||
if groupName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
|
||||
}
|
||||
if policyName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")}
|
||||
}
|
||||
if !groupExists(s3cfg, groupName) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
|
||||
}
|
||||
if e.credentialManager == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
|
||||
}
|
||||
if err := e.credentialManager.DeleteGroupInlinePolicy(context.Background(), groupName, policyName); err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
// DoActions reloads the IAM configuration for DeleteGroupPolicy, so the
|
||||
// engine deregistration via hydrateRuntimePolicies happens there. No
|
||||
// internal refresh is needed.
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListGroupPolicies is not supported in embedded IAM mode.
|
||||
// ListGroupPolicies returns the names of inline policies attached to a group.
|
||||
func (e *EmbeddedIamApi) ListGroupPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListGroupPoliciesResponse, *iamError) {
|
||||
return &iamListGroupPoliciesResponse{}, notImplementedGroupInlineError()
|
||||
resp := &iamListGroupPoliciesResponse{}
|
||||
groupName := values.Get("GroupName")
|
||||
if groupName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")}
|
||||
}
|
||||
if !groupExists(s3cfg, groupName) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)}
|
||||
}
|
||||
if e.credentialManager == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
|
||||
}
|
||||
names, err := e.credentialManager.ListGroupInlinePolicies(context.Background(), groupName)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
resp.ListGroupPoliciesResult.PolicyNames = names
|
||||
resp.ListGroupPoliciesResult.IsTruncated = false
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// handleImplicitUsername adds username who signs the request to values if 'username' is not specified.
|
||||
@@ -2492,7 +2593,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s
|
||||
glog.Errorf("Failed to reload IAM configuration after mutation: %v", err)
|
||||
// Don't fail the request since the persistent save succeeded
|
||||
}
|
||||
} else if action == "AttachUserPolicy" || action == "DetachUserPolicy" || action == "CreatePolicy" || action == "DeletePolicy" || action == "CreateUser" {
|
||||
} else if action == "AttachUserPolicy" || action == "DetachUserPolicy" || action == "CreatePolicy" || action == "DeletePolicy" || action == "CreateUser" || action == "PutGroupPolicy" || action == "DeleteGroupPolicy" {
|
||||
// Even if changed=false (persisted via credentialManager), we should still reload
|
||||
// if we are utilizing the local in-memory cache for speed
|
||||
if err := e.ReloadConfiguration(); err != nil {
|
||||
|
||||
Reference in new issue
Block a user