Skip to content

feat(iam): support accessPolicy parameter for readonly bucket access#3

Merged
kvaps merged 1 commit intoseaweedfs:mainfrom
IvanHunters:feat/access-policy-parameter
Feb 25, 2026
Merged

feat(iam): support accessPolicy parameter for readonly bucket access#3
kvaps merged 1 commit intoseaweedfs:mainfrom
IvanHunters:feat/access-policy-parameter

Conversation

@IvanHunters
Copy link
Contributor

@IvanHunters IvanHunters commented Feb 23, 2026

What this PR does

Adds support for the accessPolicy key in BucketAccessClass.parameters,
allowing COSI consumers to request read-only bucket access.

  • "readonly" grants Read and List actions only
  • "readwrite" (or empty/missing — the default) grants Read, Write, List, Tagging (preserving existing behavior)
  • Any other value returns an InvalidArgument error

Ref: kubernetes-sigs/container-object-storage-interface#244

Test plan

  • Unit tests for readonly, readwrite, default, and invalid access policies
  • All existing tests pass unchanged

Summary by CodeRabbit

  • New Features

    • Access policies now determine the exact permissions granted per bucket (policy-based actions).
  • Bug Fixes

    • Unsupported or invalid accessPolicy values are now detected and rejected with a clear error.
  • Improvements

    • Dynamic, per-bucket permission calculation replaces the previous fixed-permissions approach for more accurate access control.
  • Tests

    • Added tests validating policy-to-permission mappings and error handling for invalid policies.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b69142b and c2c27ea.

📒 Files selected for processing (2)
  • pkg/driver/provisioner.go
  • pkg/driver/provisioner_test.go

📝 Walkthrough

Walkthrough

Adds an internal helper to map accessPolicy values to IAM actions, validates policies, and uses the computed actions when granting per-bucket IAM access. Tests updated to assert policy-derived actions and to expose the fake filer for IAM inspection. (≤50 words)

Changes

Cohort / File(s) Summary
Policy-Driven IAM Actions
pkg/driver/provisioner.go
Adds non-exported actionsForPolicy to map accessPolicy to IAM actions and validate inputs. DriverGrantBucketAccess now derives per-bucket actions from accessPolicy instead of using a fixed action set; existing RMW IAM update flow preserved.
Tests & Test Helpers
pkg/driver/provisioner_test.go
Adds iamActions helper to parse/sort IAM config from the fake filer, updates newProv usage to return the fake filer, and adds TestDriverGrantBucketAccessPolicy to cover readonly, readwrite, default, and invalid policies. Existing tests adjusted to use the new helper.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • chrislusf

Poem

🐇 I hopped through policy fields at night,
Found actions hidden in soft moonlight,
No single basket fits every key,
Now each bucket sings its policy,
A carrot clap for tidy rights 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding support for the accessPolicy parameter to enable read-only bucket access in IAM configurations.
Description check ✅ Passed The description covers the main functionality (accessPolicy support with specific behavior for readonly/readwrite/invalid), test plan confirmation, and references the related issue. However, it does not mention a CHANGELOG.md update as required by the template checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @IvanHunters, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the IAM capabilities by introducing a configurable 'accessPolicy' parameter for bucket access grants. This allows consumers to request more granular permissions, specifically enabling read-only access to buckets, which improves security and adherence to the principle of least privilege. The change integrates seamlessly with existing functionality while providing robust validation for policy inputs.

Highlights

  • Configurable Access Policies: Introduced support for an 'accessPolicy' parameter within 'BucketAccessClass.parameters' to specify bucket access levels.
  • Read-Only Access: Implemented a 'readonly' policy that grants 'Read' and 'List' IAM actions, providing more granular control.
  • Default Read-Write Access: Maintained 'readwrite' as the default policy (or when the parameter is empty/missing), granting 'Read', 'Write', 'List', and 'Tagging' actions, preserving existing behavior.
  • Policy Validation: Added validation to return an 'InvalidArgument' error for any unsupported 'accessPolicy' values.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • pkg/driver/provisioner.go
    • Integrated 'accessPolicy' parameter parsing to dynamically determine IAM actions.
    • Replaced hardcoded IAM actions with actions derived from the 'accessPolicy'.
    • Added a new utility function 'actionsForPolicy' to encapsulate the logic for mapping policy strings to IAM action lists and handling invalid policies.
  • pkg/driver/provisioner_test.go
    • Updated imports to include 'sort' and 'iam_pb' for testing IAM configurations.
    • Modified the 'newProv' helper function to return the 'fakeFiler' instance, enabling inspection of its state.
    • Adjusted existing tests ('TestDriverGrantBucketAccess', 'TestDriverRevokeBucketAccess') to accommodate the updated 'newProv' signature.
    • Introduced 'iamActions' helper to parse and sort IAM actions from the 'fakeFiler''s internal buffer for verification.
    • Added a comprehensive 'TestDriverGrantBucketAccessPolicy' test suite to validate the behavior of 'readonly', 'readwrite', default, and invalid 'accessPolicy' values.
Activity
  • No specific activity (comments, reviews, or progress updates) was provided in the context.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces support for an accessPolicy parameter, allowing for granular control over bucket access (read-only or read-write). The changes are well-implemented, with clear logic for determining IAM actions based on the policy and comprehensive unit tests covering various scenarios, including invalid policies. The new actionsForPolicy function centralizes the policy logic, improving maintainability. The test suite has been appropriately extended to cover the new functionality.

IvanHunters added a commit to cozystack/cozystack that referenced this pull request Feb 23, 2026
Add a second BucketAccessClass with -ro suffix and
accessPolicy: readonly parameter to both extra and system
SeaweedFS chart templates.

Ref: seaweedfs/seaweedfs-cosi-driver#3
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
@IvanHunters IvanHunters marked this pull request as ready for review February 23, 2026 20:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/driver/provisioner_test.go (1)

69-86: Add t.Cleanup to stop the gRPC server

srv.Serve(lis) spawns a goroutine that is never stopped. Each call to newProv — including one per sub-test in TestDriverGrantBucketAccessPolicy — leaks a server goroutine and its associated listener. This can cause false positives under -race and inflated resource consumption under -count=N.

♻️ Proposed fix
 go srv.Serve(lis)
+t.Cleanup(func() { srv.Stop() })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/driver/provisioner_test.go` around lines 69 - 86, The newProv helper
starts a gRPC server (srv) and listener (lis) but never stops them, leaking
goroutines; update newProv to register a t.Cleanup that stops the server and
closes the listener (e.g. call srv.Stop() or srv.GracefulStop() and lis.Close())
so each test/subtest tears down the grpc.Server and net.Listener; locate the
newProv function and add a t.Cleanup(func(){ srv.Stop(); lis.Close() }) (or
equivalent) to ensure the provisionerServer/fakeFiler test resources are
released.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/driver/provisioner_test.go`:
- Around line 69-86: The newProv helper starts a gRPC server (srv) and listener
(lis) but never stops them, leaking goroutines; update newProv to register a
t.Cleanup that stops the server and closes the listener (e.g. call srv.Stop() or
srv.GracefulStop() and lis.Close()) so each test/subtest tears down the
grpc.Server and net.Listener; locate the newProv function and add a
t.Cleanup(func(){ srv.Stop(); lis.Close() }) (or equivalent) to ensure the
provisionerServer/fakeFiler test resources are released.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b97d29a and 425c83d.

📒 Files selected for processing (2)
  • pkg/driver/provisioner.go
  • pkg/driver/provisioner_test.go

@kvaps
Copy link
Collaborator

kvaps commented Feb 24, 2026

@IvanHunters thanks, #5 just got merged, could you please rebase?

@IvanHunters IvanHunters force-pushed the feat/access-policy-parameter branch from 425c83d to b69142b Compare February 25, 2026 08:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/driver/provisioner.go (1)

197-202: ⚠️ Potential issue | 🟠 Major

readonly grants are silently ineffective when the identity already carries write actions.

The action loop only appends missing entries; it never removes existing ones. If id already has Write:b and Tagging:b from a prior grant, a subsequent readonly grant leaves those actions in place — the identity retains full write access despite the policy. This contradicts the stated contract of accessPolicy: readonly.

The fix is to replace the per-bucket actions that overlap with the policy's action set instead of accumulating them. A minimal approach: strip any existing <action>:<bucket> entries that are not in the new actions slice before appending.

🐛 Proposed fix — replace per-bucket actions on re-grant
-	for _, a := range actions {
-		action := fmt.Sprintf("%s:%s", a, bucket)
-		if !contains(id.Actions, action) {
-			id.Actions = append(id.Actions, action)
-		}
-	}
+	// Build the desired set for this bucket under the requested policy.
+	desired := make(map[string]struct{}, len(actions))
+	for _, a := range actions {
+		desired[fmt.Sprintf("%s:%s", a, bucket)] = struct{}{}
+	}
+	// Remove any existing per-bucket actions not in the new policy.
+	kept := id.Actions[:0]
+	for _, existing := range id.Actions {
+		parts := strings.SplitN(existing, ":", 2)
+		if len(parts) == 2 && parts[1] == bucket {
+			if _, ok := desired[existing]; ok {
+				kept = append(kept, existing)
+			}
+			// else: drop — policy no longer grants this action on this bucket
+		} else {
+			kept = append(kept, existing) // different bucket or global action, keep
+		}
+	}
+	// Append any desired actions not already present.
+	for action := range desired {
+		if !contains(kept, action) {
+			kept = append(kept, action)
+		}
+	}
+	id.Actions = kept
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/driver/provisioner.go` around lines 197 - 202, The loop that appends
per-bucket actions (using variables actions, bucket and id.Actions) leaves old
actions like "Write:<bucket>" when re-granting a readonly policy; instead, first
remove from id.Actions any entries matching "<anyAction>:<bucket>" that are not
present in the new actions slice, then add the new "<action>:<bucket>" entries
as currently done (you can reuse the existing contains helper). Update the code
around the actions loop in provisioner.go (where id.Actions is mutated) to
filter out stale per-bucket actions before appending so readonly grants replace
existing write/tagging entries rather than accumulate.
🧹 Nitpick comments (2)
pkg/driver/provisioner.go (1)

338-349: LGTM — clean policy dispatch.

The three-way switch is correct: readonly{Read, List}, readwrite/"" → full set, anything else → InvalidArgument. The empty-string default aligns with the PR description's "default to readwrite" requirement.

One minor nit: the error message on line 347 says must be "readonly" or "readwrite" but omits that the empty string (i.e., omitting the key entirely) is also accepted. Consider rephrasing to avoid confusion if a consumer receives the error.

✏️ Optional wording tweak
-		return nil, fmt.Errorf("unsupported accessPolicy %q, must be \"readonly\" or \"readwrite\"", policy)
+		return nil, fmt.Errorf("unsupported accessPolicy %q; valid values are \"readonly\" and \"readwrite\" (or omit for readwrite default)", policy)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/driver/provisioner.go` around lines 338 - 349, Update the error message
in actionsForPolicy to clarify that an empty string (omitting the key) is also
accepted; specifically, change the fmt.Errorf call returned in the default case
to mention that valid values are "readonly", "readwrite", or the empty string
(defaulting to readwrite) so consumers won't be confused when they receive the
error.
pkg/driver/provisioner_test.go (1)

144-194: Add a re-grant downgrade test case to expose the permission-accumulation bug.

The four existing cases only cover fresh grants. The bug in DriverGrantBucketAccess (actions are never removed on re-grant) is not exercised because every case starts with a clean newProv. A case that first grants readwrite, then re-grants readonly on the same identity, would have caught it.

🧪 Suggested additional test case
// After the existing cases, add to TestDriverGrantBucketAccessPolicy:
t.Run("downgrade readwrite to readonly", func(t *testing.T) {
    p, ff := newProv(t)
    base := &cosispec.DriverGrantBucketAccessRequest{
        BucketId: "b", Name: "u",
        Parameters: map[string]string{"accessPolicy": "readwrite"},
    }
    if _, err := p.DriverGrantBucketAccess(context.Background(), base); err != nil {
        t.Fatalf("initial grant: %v", err)
    }
    req := &cosispec.DriverGrantBucketAccessRequest{
        BucketId: "b", Name: "u",
        Parameters: map[string]string{"accessPolicy": "readonly"},
    }
    if _, err := p.DriverGrantBucketAccess(context.Background(), req); err != nil {
        t.Fatalf("readonly re-grant: %v", err)
    }
    got := iamActions(t, ff, "u")
    want := []string{"List:b", "Read:b"}
    if !reflect.DeepEqual(got, want) {
        t.Errorf("actions after downgrade = %v, want %v", got, want)
    }
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/driver/provisioner_test.go` around lines 144 - 194, Add a test that
re-grants with lower privileges to exercise the permission-accumulation bug:
inside TestDriverGrantBucketAccessPolicy use newProv() to create p and ff, call
p.DriverGrantBucketAccess with parameters {"accessPolicy":"readwrite"} for Name
"u", then call p.DriverGrantBucketAccess again with {"accessPolicy":"readonly"}
for the same Name "u", and finally assert via iamActions(t, ff, "u") that the
resulting actions equal only {"List:b","Read:b"} (and fail the test if not);
this will reveal if DriverGrantBucketAccess fails to remove actions on
downgrade.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/driver/provisioner_test.go`:
- Around line 69-86: newProv currently starts srv.Serve in a goroutine and never
stops it, leaking goroutines and the TCP listener; register a t.Cleanup that
stops the gRPC server and closes the listener (e.g., call srv.GracefulStop() or
srv.Stop() and lis.Close()) so the goroutine running srv.Serve returns and the
socket is released; add this cleanup in newProv (referencing symbols newProv,
srv, lis, provisionerServer, NewProvisionerServer) immediately after starting
the server (or after creating p) to ensure resources are cleaned up when the
test finishes.

---

Outside diff comments:
In `@pkg/driver/provisioner.go`:
- Around line 197-202: The loop that appends per-bucket actions (using variables
actions, bucket and id.Actions) leaves old actions like "Write:<bucket>" when
re-granting a readonly policy; instead, first remove from id.Actions any entries
matching "<anyAction>:<bucket>" that are not present in the new actions slice,
then add the new "<action>:<bucket>" entries as currently done (you can reuse
the existing contains helper). Update the code around the actions loop in
provisioner.go (where id.Actions is mutated) to filter out stale per-bucket
actions before appending so readonly grants replace existing write/tagging
entries rather than accumulate.

---

Nitpick comments:
In `@pkg/driver/provisioner_test.go`:
- Around line 144-194: Add a test that re-grants with lower privileges to
exercise the permission-accumulation bug: inside
TestDriverGrantBucketAccessPolicy use newProv() to create p and ff, call
p.DriverGrantBucketAccess with parameters {"accessPolicy":"readwrite"} for Name
"u", then call p.DriverGrantBucketAccess again with {"accessPolicy":"readonly"}
for the same Name "u", and finally assert via iamActions(t, ff, "u") that the
resulting actions equal only {"List:b","Read:b"} (and fail the test if not);
this will reveal if DriverGrantBucketAccess fails to remove actions on
downgrade.

In `@pkg/driver/provisioner.go`:
- Around line 338-349: Update the error message in actionsForPolicy to clarify
that an empty string (omitting the key) is also accepted; specifically, change
the fmt.Errorf call returned in the default case to mention that valid values
are "readonly", "readwrite", or the empty string (defaulting to readwrite) so
consumers won't be confused when they receive the error.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 425c83d and b69142b.

📒 Files selected for processing (2)
  • pkg/driver/provisioner.go
  • pkg/driver/provisioner_test.go

Comment on lines +69 to 86
func newProv(t *testing.T) (*provisionerServer, *fakeFiler) {
t.Helper()

lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
ff := &fakeFiler{}
srv := grpc.NewServer()
filer_pb.RegisterSeaweedFilerServer(srv, &fakeFiler{})
filer_pb.RegisterSeaweedFilerServer(srv, ff)
go srv.Serve(lis)

p, err := NewProvisionerServer("prov", lis.Addr().String(), "", "", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("init prov: %v", err)
}
return p.(*provisionerServer)
return p.(*provisionerServer), ff
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Register t.Cleanup to stop the gRPC server and avoid goroutine/socket leaks.

srv.Serve is launched in a goroutine but is never stopped; each call to newProv leaks a goroutine and a TCP listener for the duration of the test binary. Under -count=N or with the race detector this can cause resource-exhaustion flakiness.

🛠️ Proposed fix
 	go srv.Serve(lis)
+	t.Cleanup(srv.GracefulStop)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/driver/provisioner_test.go` around lines 69 - 86, newProv currently
starts srv.Serve in a goroutine and never stops it, leaking goroutines and the
TCP listener; register a t.Cleanup that stops the gRPC server and closes the
listener (e.g., call srv.GracefulStop() or srv.Stop() and lis.Close()) so the
goroutine running srv.Serve returns and the socket is released; add this cleanup
in newProv (referencing symbols newProv, srv, lis, provisionerServer,
NewProvisionerServer) immediately after starting the server (or after creating
p) to ensure resources are cleaned up when the test finishes.

Add actionsForPolicy() to determine IAM actions based on the
accessPolicy key in BucketAccessClass.parameters:
- "readonly" grants Read and List
- "readwrite" or empty (default) grants Read, Write, List, Tagging
- invalid values return InvalidArgument error

Ref: kubernetes-sigs/container-object-storage-interface#244
Signed-off-by: ohotnikov.ivan <ohotnikov.ivan@e-queo.net>
@kvaps kvaps force-pushed the feat/access-policy-parameter branch from b69142b to c2c27ea Compare February 25, 2026 21:10
@kvaps kvaps merged commit af8cce4 into seaweedfs:main Feb 25, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants