feat(drive): improve Drive model
now Drive is purely a metadata
each drive has always a root folder
this model minimize Oxicloud changes, and simplify
the Drive name is simply the folder's root's name
note: owner of Drive has more permission that an owner of the root folder
This commit is contained in:
@@ -48,8 +48,14 @@ jsonpath "$" count >= 1
|
||||
jsonpath "$[0].kind" == "personal"
|
||||
jsonpath "$[0].default_for_user" == "{{admin_user_id}}"
|
||||
jsonpath "$[0].name" == "Personal"
|
||||
# root_folder_id surfaces the drive's mount-point folder. Sourced via
|
||||
# JOIN from storage.folders.name — drives have no `name` column under
|
||||
# the D0 design (docs/plan/drive.md §3). Folder API operations
|
||||
# (create-in-drive, rename-drive) all key off this id.
|
||||
jsonpath "$[0].root_folder_id" exists
|
||||
[Captures]
|
||||
admin_drive_id: jsonpath "$[0].id"
|
||||
admin_root_folder_id: jsonpath "$[0].root_folder_id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -110,6 +116,8 @@ HTTP 200
|
||||
jsonpath "$" count == 1
|
||||
jsonpath "$[0].kind" == "personal"
|
||||
jsonpath "$[0].default_for_user" == "{{alice_user_id}}"
|
||||
jsonpath "$[0].name" == "Personal"
|
||||
jsonpath "$[0].root_folder_id" exists
|
||||
[Captures]
|
||||
alice_drive_id: jsonpath "$[0].id"
|
||||
|
||||
@@ -122,6 +130,8 @@ HTTP 200
|
||||
jsonpath "$" count == 1
|
||||
jsonpath "$[0].kind" == "personal"
|
||||
jsonpath "$[0].default_for_user" == "{{bob_user_id}}"
|
||||
jsonpath "$[0].name" == "Personal"
|
||||
jsonpath "$[0].root_folder_id" exists
|
||||
[Captures]
|
||||
bob_drive_id: jsonpath "$[0].id"
|
||||
|
||||
|
||||
@@ -281,12 +281,16 @@ jsonpath "$.items[*].resource.name" not contains "bob-attack-2"
|
||||
# WebDAV MKCOL — namespace isolation
|
||||
# ═════════════════════════════════════════════════════════════
|
||||
# WebDAV requests are isolated per-user by `resolve_webdav_path`
|
||||
# (webdav_handler.rs:189). If the requested path doesn't begin
|
||||
# with the caller's home folder name ("My Folder - <username>"),
|
||||
# the handler silently prefixes the caller's home folder path
|
||||
# onto the front. Effect: any WebDAV path a client sends is
|
||||
# always resolved INSIDE the caller's own tree, regardless of
|
||||
# what they wrote.
|
||||
# (webdav_handler.rs:235). If the requested path doesn't begin
|
||||
# with the caller's home folder name (the drive's root folder
|
||||
# name — "Personal" by default post-D0), the handler silently
|
||||
# prefixes the caller's home folder path onto the front. Effect:
|
||||
# any WebDAV path a client sends is always resolved INSIDE the
|
||||
# caller's own tree, regardless of what they wrote.
|
||||
# The test URLs below use "My Folder - <username>" as a path
|
||||
# segment that's GUARANTEED not to match any caller's home name
|
||||
# (all home folders are "Personal" post-D0), so the resolver's
|
||||
# prepend branch always fires.
|
||||
#
|
||||
# These tests assert the isolation works (regression guard) and
|
||||
# that the service-level verify_owner still acts as
|
||||
@@ -307,8 +311,13 @@ HTTP 201
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 17 – Positive control: bob MKCOL inside his own home.
|
||||
# Uses "Personal" — bob's home folder name post-D0
|
||||
# (docs/plan/drive.md §3, the canonical default). The resolver
|
||||
# detects the URL already starts with the caller's home name and
|
||||
# does NOT prepend again, so the new folder lands directly in
|
||||
# bob's home rather than in a fresh intermediate.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
MKCOL {{base_url}}/webdav/My%20Folder%20-%20bob/bob-webdav-own
|
||||
MKCOL {{base_url}}/webdav/Personal/bob-webdav-own
|
||||
Authorization: Bearer {{bob_token}}
|
||||
|
||||
HTTP 201
|
||||
|
||||
@@ -43,39 +43,50 @@ psql -v ON_ERROR_STOP=1 -c "
|
||||
" >/dev/null
|
||||
|
||||
# The OxiCloud server normally provisions a default Personal drive +
|
||||
# Owner role_grant on user creation via PersonalDriveLifecycleHook
|
||||
# (D0). This script bypasses that pipeline — it INSERTs directly into
|
||||
# auth.users — so we mirror the hook's behaviour here. Without it,
|
||||
# integration test fixtures that hand-roll INSERTs into storage.files
|
||||
# fail with "drive_id not-null violation" (M3 made the column
|
||||
# mandatory), and helpers that JOIN auth.users with storage.drives
|
||||
# return RowNotFound.
|
||||
# its root folder + Owner role_grant on user creation via
|
||||
# PersonalDriveLifecycleHook (D0). This script bypasses that pipeline
|
||||
# — it INSERTs directly into auth.users — so we mirror the hook's
|
||||
# behaviour here. Without it, integration test fixtures that hand-roll
|
||||
# INSERTs into storage.files fail with "drive_id not-null violation"
|
||||
# (M3 made the column mandatory), and helpers that JOIN auth.users
|
||||
# with storage.drives return RowNotFound.
|
||||
#
|
||||
# Four sequential writes inside one transaction (docs/plan/drive.md §3):
|
||||
# drive + root folder + drives.root_folder_id wire-up + Owner role_grant.
|
||||
# A single CTE would be more compact but doesn't work — PG's CTE
|
||||
# sub-statements share an MVCC snapshot, so a later branch's UPDATE
|
||||
# can't match a row inserted by an earlier branch. The transaction
|
||||
# form is the production path's shape (DrivePgRepository::create_personal_drive_atomic).
|
||||
# Idempotency: skipped on retry by the `default_for_user` precondition.
|
||||
echo "[init-schema] provisioning ci-admin's default Personal drive (idempotent)"
|
||||
psql -v ON_ERROR_STOP=1 <<'SQL' >/dev/null
|
||||
WITH admin AS (
|
||||
SELECT id FROM auth.users WHERE username = 'ci-admin'
|
||||
),
|
||||
ins_drive AS (
|
||||
INSERT INTO storage.drives (name, kind, default_for_user, quota_bytes)
|
||||
SELECT 'Personal', 'personal', admin.id, NULL
|
||||
FROM admin
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM storage.drives d WHERE d.default_for_user = admin.id
|
||||
)
|
||||
RETURNING id, default_for_user
|
||||
)
|
||||
INSERT INTO storage.role_grants
|
||||
(subject_type, subject_id, resource_type, resource_id, role, granted_by)
|
||||
SELECT 'user', ins_drive.default_for_user, 'drive', ins_drive.id, 'owner',
|
||||
ins_drive.default_for_user
|
||||
FROM ins_drive
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM storage.role_grants g
|
||||
WHERE g.subject_type = 'user'
|
||||
AND g.subject_id = ins_drive.default_for_user
|
||||
AND g.resource_type = 'drive'
|
||||
AND g.resource_id = ins_drive.id
|
||||
);
|
||||
DO $$
|
||||
DECLARE
|
||||
admin_id uuid;
|
||||
drive_id uuid;
|
||||
folder_id uuid;
|
||||
BEGIN
|
||||
SELECT id INTO admin_id FROM auth.users WHERE username = 'ci-admin';
|
||||
IF EXISTS (SELECT 1 FROM storage.drives WHERE default_for_user = admin_id) THEN
|
||||
RETURN; -- already provisioned, idempotent no-op
|
||||
END IF;
|
||||
|
||||
INSERT INTO storage.drives (kind, default_for_user, quota_bytes)
|
||||
VALUES ('personal', admin_id, NULL)
|
||||
RETURNING id INTO drive_id;
|
||||
|
||||
INSERT INTO storage.folders
|
||||
(name, parent_id, user_id, drive_id, created_by, updated_by)
|
||||
VALUES ('Personal', NULL, admin_id, drive_id, admin_id, admin_id)
|
||||
RETURNING id INTO folder_id;
|
||||
|
||||
UPDATE storage.drives SET root_folder_id = folder_id WHERE id = drive_id;
|
||||
|
||||
INSERT INTO storage.role_grants
|
||||
(subject_type, subject_id, resource_type, resource_id, role, granted_by)
|
||||
VALUES ('user', admin_id, 'drive', drive_id, 'owner', admin_id);
|
||||
END
|
||||
$$;
|
||||
SQL
|
||||
|
||||
echo "[init-schema] done"
|
||||
|
||||
Reference in New Issue
Block a user