Skip to content

feat(cosi): foundation admin client + CAS grant ownership - #219

Open
BenjaminFuentesEviden wants to merge 6 commits into
rustfs:mainfrom
BenjaminFuentesEviden:feat/cosi-foundation-ownership
Open

feat(cosi): foundation admin client + CAS grant ownership#219
BenjaminFuentesEviden wants to merge 6 commits into
rustfs:mainfrom
BenjaminFuentesEviden:feat/cosi-foundation-ownership

Conversation

@BenjaminFuentesEviden

Copy link
Copy Markdown

Type of Change

  • New Feature
  • Bug Fix
  • Documentation
  • Performance Improvement
  • Test/CI
  • Refactor
  • Other:

Related Issues

Summary of Changes

Foundation PR A (COSI+Helm / sidecar pin stay in PR B):

  1. Extract crates/rustfs-admin — kube-agnostic HTTP/admin/S3/STS client shared by operator and rustfs-cosi-driver (operator keeps Tenant credential loading wrappers).
  2. Durable random credentials — 40+ char random secret persisted in namespaced Secret cosi-cred-{grant} before add_user; retries reuse the Secret (never re-derive from access key).
  3. CAS grant ownership — ConfigMap rustfs-cosi-ownership checkpoints PendingCreateReady (proof: grant_name, account_id, access_key_hash, cred_secret_name, state). Concurrent preferredAccessKey conflicts; orphan RustFS users are not adopted.
  4. Unique policies — generated cosi-pol-{grant}; BAC policy param is validate/attach only (never add_canned_policy replace).
  5. Split driver state machine — thin gRPC adapters over grant / bucket modules; static bucketName/buckets deletes return FailedPrecondition.

Checklist

  • I have read and followed the CONTRIBUTING.md guidelines
  • Passed make pre-commit (fmt-check + clippy + test + console-lint + console-fmt-check)
  • Added/updated necessary tests
  • Documentation updated (if needed)
  • CHANGELOG.md updated under [Unreleased] (if user-visible change)
  • CI/CD passed (if applicable)

Impact

  • Breaking change (CRD/API compatibility)
  • Requires doc/config/deployment update
  • Other impact: COSI driver grant/secret ownership model changes; Helm Deployment remains PR B

Verification

cargo test -p rustfs-admin
cargo test -p rustfs-cosi-driver --bins
cargo clippy -p rustfs-admin -p rustfs-cosi-driver -- -D warnings
make fmt-check

Additional Notes


Thank you for your contribution! Please ensure your PR follows the community standards (CODE_OF_CONDUCT.md) and sign the CLA if this is your first contribution.

BenjaminFuentesEviden and others added 6 commits August 3, 2026 17:48
Extract shared rustfs-admin client, ship a tonic COSI driver with Helm
toggle, and document BucketClass parameters for Tenant-backed S3.

Co-authored-by: Cursor <cursoragent@cursor.com>
Align DriverGrantBucketAccess with Ceph-style isolation: deterministic
secrets, never rotate existing users, and return AlreadyExists when
preferredAccessKey is claimed by another BucketAccess.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace derivable secrets and marker-based IAM ownership with durable
random credentials plus PendingCreate/Ready checkpoints so concurrent
preferredAccessKey grants conflict safely and external BAC policies are
never overwritten. Split the COSI driver into thin gRPC adapters over
testable grant/bucket state machines.

Co-authored-by: Cursor <cursoragent@cursor.com>
Existence probes and test fixtures often return an empty 200; treat that
as an existing user with no policies instead of failing the parse.

Co-authored-by: Cursor <cursoragent@cursor.com>
@BenjaminFuentesEviden

Copy link
Copy Markdown
Author

@GatewayJ I applied the first PR here that is mainly impacting the code split you asked, once validated, I will add the extras on a next PR B

@GatewayJ

GatewayJ commented Aug 9, 2026

Copy link
Copy Markdown
Member

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb52aee2b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +130 to +132
if let Some(owner) = self.find_by_account_id(account_id).await?
&& owner != grant_name
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make the account claim atomic with the CAS write

When two grants concurrently request the same preferredAccessKey, both can pass this lookup before either proof is written. The losing cas_put then retries against the updated ConfigMap and inserts its own grant key without rechecking the account owner, leaving two proofs for one RustFS account; subsequent policy attachment can overwrite the other grant's access. Revalidate the uniqueness predicate on every CAS retry or represent the account claim with a single atomically created key.

Useful? React with 👍 / 👎.

Comment on lines +181 to +185
api.patch(
secret_name,
&PatchParams::apply("rustfs-cosi-driver").force(),
&Patch::Apply(&secret),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Create the credential Secret without force-overwriting races

When concurrent retries first provision the same grant, both callers can observe a 404, generate different secret keys, and force-apply this Secret while continuing with their respective local values. Kubernetes Secret write order and RustFS add_user order are independent, so the persisted Secret can contain a key different from the one accepted by RustFS, immediately returning unusable credentials. Use create-once semantics and reload the persisted values after an AlreadyExists conflict.

Useful? React with 👍 / 👎.

Comment on lines +135 to +139
data.insert(
"accessKeyID".to_string(),
ByteString(access_key.as_bytes().to_vec()),
);
data.insert(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Persist the required canonical credential keys

The durable credential Secret writes accessKeyID/accessSecretKey and AWS aliases, but never the required exact accesskey and secretkey keys; the returned COSI credential map has the same omission. Consequently these generated Credential Secrets violate the repository credential contract and cannot be consumed by code expecting the canonical Tenant key names. Add the canonical keys while retaining any compatibility aliases.

AGENTS.md reference: AGENTS.md:L60-L62

Useful? React with 👍 / 👎.

use k8s_openapi::api::core::v1::{ConfigMap, Secret};
use kube::{Api, Client};
use rustfs_admin::RustfsAdminClient;
use thiserror::Error;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use snafu for the new COSI error types

The new COSI crate introduces thiserror-derived error enums in backend, bucket, credentials, grant, and ownership, contrary to the repository-wide requirement that error handling use snafu. Convert these new errors to the established snafu context pattern rather than adding a second error framework.

AGENTS.md reference: AGENTS.md:L64-L68

Useful? React with 👍 / 👎.

@@ -0,0 +1,146 @@
//! Admin credential lookup + RustFS admin client construction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add Apache headers to the new COSI files

The newly added COSI source files begin directly with module documentation and omit the mandatory Apache 2.0 license header; the same omission affects the other new COSI Rust sources, build.rs, and the protocol source. Add the required header to each new non-generated file.

AGENTS.md reference: AGENTS.md:L64-L68

Useful? React with 👍 / 👎.

Comment on lines +124 to +126
let msg = err.to_string();
if msg.contains("not found") || msg.contains("NoSuch") {
GrantError::MissingExternalPolicy(policy_name.clone())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Classify ordinary 404 policy responses as missing

When an external policy lookup returns a 404 with an empty or generic response body, the error string is upstream returned 404 Not Found; the case-sensitive checks here match neither lowercase not found nor NoSuch. The request is consequently reported as an internal admin failure rather than the intended MissingExternalPolicy/FailedPrecondition, causing controllers to retry a permanently invalid BucketAccessClass. Inspect the structured status or normalize the message before classifying it.

Useful? React with 👍 / 👎.

Comment thread src/sts/tests.rs
Comment on lines +1008 to +1009
#[tokio::test]
async fn get_user_info_parses_comma_separated_policy_names() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reconnect the RustFS client integration tests

These newly added tests, along with the existing tests in src/sts/tests.rs, are no longer compiled because this commit removes #[cfg(test)] #[path = "tests.rs"] mod tests; from rustfs_client.rs and no other module references the file. As a result, cargo test silently skips the HTTP signing, error-redaction, TLS, pool, bucket, and user-operation suite; move the tests to rustfs-admin or restore a test-module declaration.

Useful? React with 👍 / 👎.

Comment on lines +110 to +114
pub fn primary_bucket_id(&self, cosi_name: &str) -> String {
self.bucket_name
.clone()
.or_else(|| self.buckets_to_create(cosi_name).into_iter().next())
.unwrap_or_else(|| cosi_name.to_string())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject simultaneous bucketName and buckets overrides

When both overrides are supplied, buckets_to_create gives buckets precedence, while this method gives bucketName precedence. For example, bucketName=foo,buckets=bar creates bar but returns foo as the COSI bucket_id, so subsequent consumers are told to use a bucket that was never created. Reject the conflicting parameters or use the same precedence for creation and the returned identifier.

Useful? React with 👍 / 👎.

Comment on lines +109 to +112
Err(KubeError::Api(err)) if err.code == 404 => {
let secret_key = random_secret_key(40);
persist_credentials(kube, namespace, &secret_name, access_key, &secret_key).await?;
Ok(StoredCredentials {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not regenerate credentials for an existing user

If the durable credential Secret is deleted while its ownership proof and RustFS user still exist, an idempotent grant retry enters this 404 branch and persists a newly generated secret. The later user-exists path only reattaches policies and never updates the RustFS user's password, so the RPC returns credentials that cannot authenticate and permanently replaces the only stored secret with the invalid value. When a proof or user already exists, treat a missing Secret as an unrecoverable conflict or explicitly rotate the RustFS credential in the same state transition.

Useful? React with 👍 / 👎.

Comment on lines +63 to +68
match client
.create_bucket(bucket, Some(params.region.as_str()), false)
.await
.map_err(|err| BucketError::Admin(err.to_string()))?
{
CreateBucketResult::Created | CreateBucketResult::AlreadyExists => {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refuse to adopt pre-existing dynamic buckets

On the dynamic path, any BucketAlreadyExists or BucketAlreadyOwnedByYou response is accepted without recording ownership. An existing bucket whose name matches the COSI request is therefore reported as provisioned, and DriverDeleteBucket later unconditionally deletes it because no static override is present; another RustFS user's pre-created bucket can consequently lose all of its data. Distinguish a driver-owned idempotent retry from an unowned name collision before returning success.

Useful? React with 👍 / 👎.

@GatewayJ

GatewayJ commented Aug 9, 2026

Copy link
Copy Markdown
Member

Request changes: COSI idempotency and ownership guarantees are not satisfied

The overall direction is good: extracting rustfs-admin, keeping the gRPC adapter thin, using durable random credentials, and separating generated policies from externally managed policies are all sound design choices.

However, the current implementation does not yet satisfy the retry, idempotency, and resource-identity guarantees required by the official [COSI v0.2 specification](https://github.com/kubernetes-sigs/container-object-storage-interface/blob/release-0.2/proto/spec.md) and [protocol definition](https://github.com/kubernetes-sigs/container-object-storage-interface/blob/release-0.2/proto/cosi.proto).

COSI RPCs may time out and be retried. A retry with the same fields must continue the original operation safely. DriverCreateBucket must also return success only for a compatible existing bucket and ALREADY_EXISTS for an incompatible name collision.

Blocking correctness issues

  1. Dynamic bucket creation can adopt and later delete an unrelated bucket

    create_bucket treats both BucketAlreadyExists and BucketAlreadyOwnedByYou as successful without recording or validating ownership. DriverDeleteBucket then unconditionally deletes dynamic buckets.

    This violates DriverCreateBucket idempotency semantics: an existing bucket is only a successful retry when it represents the same compatible operation. Otherwise, the driver must return ALREADY_EXISTS.

    Please persist a bucket ownership/idempotency record containing at least the backend identity, COSI bucket name, returned bucket ID, and relevant parameters. Do not delete a bucket without matching ownership proof.

  2. The preferred access-key claim is not atomic

    begin_or_resume checks find_by_account_id before entering cas_put. When the ConfigMap replace receives 409, cas_put reloads and writes without revalidating account ownership.

    Two concurrent grants can therefore both claim the same preferredAccessKey, leaving multiple proofs for one RustFS account. Concurrent calls for the same grant but different parameters may also overwrite each other.

    The uniqueness predicate must be part of every CAS retry. Prefer an atomically created per-account claim keyed by {backend identity, account ID}, rather than a shared ConfigMap containing all grants.

  3. Credential persistence is not create-once

    Two retries can both observe a missing Secret, generate different passwords, and force-apply the same Secret. Kubernetes write order and RustFS add_user order are independent, so the stored Secret may not match the password accepted by RustFS.

    Create the Secret once. On AlreadyExists, reload and use the persisted credentials. Do not use force-apply for credential creation.

  4. A missing durable Secret produces invalid credentials

    If the ownership proof and RustFS user exist but the internal credential Secret was deleted, the retry generates a new password. The existing-user path only reattaches policy and does not rotate the RustFS password, so the RPC returns credentials that cannot authenticate.

    This condition must either fail as an unrecoverable consistency conflict or perform an explicit, atomic credential rotation.

  5. A stale revoke can delete a newer grant

    DriverRevokeBucketAccess locates ownership only by account_id. After grant A is revoked, the same preferred key can be assigned to grant B. A delayed retry for A can then remove B’s proof and delete B’s RustFS user.

    Because account_id is the identifier later supplied to revoke access, it must identify one grant generation unambiguously. Use an opaque, non-reused account ID or retain a tombstone/generation that prevents stale revoke requests from targeting newer ownership.

  6. Upstream errors can leak credentials

    The extracted rustfs-admin client removed the existing sensitive-field redaction before returning UnexpectedStatus. RustFS JSON, XML, or text errors containing secretkey, SecretAccessKey, or AccessKeyId can now reach logs and gRPC status messages.

    Restore redaction inside the shared client before errors cross crate boundaries.

Required tests

Please add regression tests that would fail with the current implementation:

  • Two grants concurrently requesting the same preferred access key.
  • Two concurrent retries creating the same credential Secret.
  • Crash/retry after each transition: ownership claim, Secret creation, user creation, policy attachment, and Ready promotion.
  • Retry when the internal Secret is missing but the RustFS user exists.
  • Delayed revoke after the account name has been reused.
  • Existing compatible versus incompatible bucket collisions.
  • Delete refusal without matching bucket ownership proof.
  • Upstream error redaction.

The existing src/sts/tests.rs suite is no longer connected to the module tree, so the HTTP signing, TLS, admin-operation, and redaction tests must also be moved to rustfs-admin or reconnected.

Repository requirements

Before re-review, please also:

  • Restore the required accesskey and secretkey credential keys.
  • Use snafu instead of introducing thiserror.
  • Add the required Apache 2.0 headers to all new source and protocol files.
  • Reject conflicting bucketName and buckets parameters.
  • Use structured HTTP status/error information instead of string matching to classify missing external policies.
  • Resolve the current conflicts with main.
  • Run and report make pre-commit; validate the modified Dockerfile because the PR Docker job is currently skipped.

CI being green confirms compilation and existing checks, but it does not cover the ownership races, crash recovery, stale revocation, or destructive bucket path described above.

Once these state-machine and ownership guarantees are fixed and covered by fault-oriented tests, the architecture will be in a much better position for the follow-up deployment PR.

@GatewayJ GatewayJ assigned GatewayJ and unassigned GatewayJ Aug 9, 2026
@GatewayJ
GatewayJ self-requested a review August 9, 2026 12:28
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.

2 participants