@@ -402,6 +502,54 @@
text-decoration: underline;
}
+ /* Drive title row: icon + name (or inline rename input) + edit/save
+ affordances. Mirrors the visual weight of the previous static
+
so the page layout doesn't shift when entering rename mode. */
+ .drive-title {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ }
+
+ .drive-title__name {
+ margin: 0;
+ }
+
+ .drive-title__input {
+ flex: 1;
+ min-width: 0;
+ max-width: 28rem;
+ padding: 0.4rem 0.6rem;
+ font-size: 1.5rem;
+ font-weight: var(--weight-semibold, 600);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background: var(--color-bg-input);
+ color: var(--color-text);
+ }
+
+ /* Compact icon button used in the title row + nowhere else here.
+ The shared `.icon-btn` style isn't promoted to a global yet, so
+ we duplicate the minimum that this page needs. */
+ .icon-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background: var(--color-bg-surface);
+ color: var(--color-text);
+ cursor: pointer;
+ }
+
+ .icon-btn:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ }
+
/* Members card header: title on the left, "Manage members" button on
the right when the caller can mutate membership. */
.members__header {
diff --git a/src/application/services/folder_service.rs b/src/application/services/folder_service.rs
index 8ec34bf7..9b5cb9fc 100644
--- a/src/application/services/folder_service.rs
+++ b/src/application/services/folder_service.rs
@@ -446,10 +446,32 @@ impl FolderUseCase for FolderService {
)));
}
+ // Drive roots double as the drive's display name (per drive.md §3,
+ // `drives.name` is sourced from `storage.folders.name` of the row
+ // pointed at by `root_folder_id`). Per drive.md §6 the rename is
+ // Owner-only — but with `Permission::Update` that's leaky because
+ // every Editor of the drive has Update on every folder in the
+ // drive, including the root. So we promote the requirement to
+ // `Manage` for root folders. A root is identified by
+ // `parent_id IS NULL`; that's the same property the drive seeder
+ // and the drive-of-resource resolver rely on, so no schema-level
+ // assumption shifts here.
+ let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
+ DomainError::internal_error(
+ "FolderStorage",
+ format!("Failed to look up folder before rename: {id}: {e}"),
+ )
+ })?;
+ let required_perm = if folder.parent_id().is_none() {
+ Permission::Manage
+ } else {
+ Permission::Update
+ };
+
self.authz
.require(
Subject::User(caller_id),
- Permission::Update,
+ required_perm,
Self::folder_resource(id)?,
)
.await?;
diff --git a/tests/api/drives_membership.hurl b/tests/api/drives_membership.hurl
index 80070d37..58d4f872 100644
--- a/tests/api/drives_membership.hurl
+++ b/tests/api/drives_membership.hurl
@@ -704,30 +704,19 @@ HTTP 404
# 26d — Editor renames the drive (root folder) → 404.
-# Editor bundle has Update on content, but the rename
-# endpoint uses authz.require(...) and Editor's bundle on
-# the drive root resolves through the same drive precheck.
-# Editor has Update; folder rename uses Update; so this
-# should actually SUCCEED. Asserting 200 to reflect the
-# real engine semantics — the "Editor can rename the drive"
-# fact is a real product question worth surfacing here.
-# If you want rename to be Owner-only, the fix is in the
-# folder service (require Manage, not Update).
+# Folder rename normally requires `Permission::Update` (which
+# Editor has on every folder in the drive via the engine's drive
+# precheck). The folder service promotes the requirement to
+# `Permission::Manage` when the target folder has `parent_id IS
+# NULL` — i.e. it's a drive root — so the drive-rename surface is
+# Owner-only per drive.md §6, without changing the public folder
+# endpoint shape. Anti-enum: refusal returns 404 (not 403).
PUT {{base_url}}/api/folders/{{team_root_folder_id}}/rename
Authorization: Bearer {{bob_token}}
Content-Type: application/json
{ "name": "team-drive-editor-renamed" }
-HTTP 200
-
-
-# Restore the previous name so downstream assertions don't drift.
-PUT {{base_url}}/api/folders/{{team_root_folder_id}}/rename
-Authorization: Bearer {{alice_token}}
-Content-Type: application/json
-{ "name": "team-drive-renamed" }
-
-HTTP 200
+HTTP 404
# ─────────────────────────────────────────────────────────────