Skip to content

feat(server-api): add /api/shares with link role, expiry, and password - #2807

Merged
giswqs merged 5 commits into
opengeos:mainfrom
deniial00:deniial00/active-share
Oct 2, 2026
Merged

giswqs merged 5 commits into
opengeos:mainfrom
deniial00:deniial00/active-share

Conversation

@deniial00

@deniial00 deniial00 commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Closes #2803

Problem

Project > Share > Active Shares fails with Failed to fetch shares (HTTP 404). The client (added in #1534) calls GET /api/shares and DELETE /api/shares/{id}, but geolibre_server_api never implemented them, nor the role, expiry and password fields the client sends.

Changes

  • GET /api/shares: the caller's managed public and unlisted projects. The share id is the project id.
  • DELETE /api/shares/{id}: makes the project private and resets role, expiry and password. The project, its versions and group shares are kept. Organization-visible projects are not link shares and return 404.
  • POST /api/projects accepts role, expiresIn and password. Project JSON returns role, expiresAt and hasPassword.
  • Expiry (410) and password (401) are enforced on every non-manager read. POST /{username}/{slug}/access and POST /org/{org}/{slug}/access unlock a link. Wrong guesses are throttled to 10 per 5 minutes per project and client address (in memory, per process). role is stored and returned only.
  • New columns share_role, share_expires_at, share_password_hash are added to the Postgres and SQLite upgrade paths, including the SQLite table rebuild.
  • docs/server-api.md documents the routes and fields.

Testing

pytest backend/geolibre_server_api/tests: 220 passed (10 new in test_shares.py). ruff check and ruff format are clean.

Deployment

The hosted share.geolibre.app returns 404 until this backend is deployed. The client needs no change.

Summary by CodeRabbit

  • New Features
    • Public and unlisted project links can now have view, comment, or edit access, an optional expiration, and password protection.
    • Manageable share links can be listed and revoked. Revoking a link makes its project private without deleting the project or its versions.
    • Password-protected links can be unlocked; repeated incorrect attempts are temporarily limited.
  • Access Updates
    • Expired links and password-protected content require appropriate access. Managers can still access shares regardless of these link settings.

opengeos#2803)

The Share dialog's Active Shares tab calls GET/DELETE /api/shares, which the
projects server never implemented, so it failed with HTTP 404.

- GET /api/shares lists the caller's managed public/unlisted projects.
- DELETE /api/shares/{id} makes the project private and resets link settings,
  keeping the project, versions, and group shares.
- POST /api/projects accepts role, expiresIn, password; project JSON returns
  role, expiresAt, hasPassword.
- Expiry (410) and password (401) are enforced on every non-manager read;
  POST /{username}/{slug}/access and /org/{org}/{slug}/access unlock a link,
  throttled to 10 wrong guesses per 5 minutes.
Copilot AI balanced review requested due to automatic review settings October 2, 2026 20:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: ba722760-23e6-465a-841b-57a9ca6e7f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 540fc2e and 6704a84.

📒 Files selected for processing (4)
  • backend/geolibre_server_api/geolibre_server_api/main.py
  • backend/geolibre_server_api/geolibre_server_api/project_models.py
  • backend/geolibre_server_api/tests/test_shares.py
  • docs/server-api.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

Project shares now support roles, expiry, and password protection. The API adds endpoints to list and revoke manageable shares, and routes to unlock password-protected links. Database upgrades preserve the new share settings.

Changes

Share links

Layer / File(s) Summary
Share settings and project creation
backend/geolibre_server_api/geolibre_server_api/main.py, backend/geolibre_server_api/geolibre_server_api/project_models.py, backend/geolibre_server_api/tests/test_shares.py, docs/server-api.md
Project creation accepts role, expiry, and password settings for public and unlisted shares. The API stores these settings, returns them in project data, and rejects incompatible visibility settings. Database upgrade paths add the corresponding fields. Tests and API documentation cover creation, defaults, and validation.
Password and expiry access
backend/geolibre_server_api/geolibre_server_api/main.py, backend/geolibre_server_api/tests/test_shares.py, docs/server-api.md
Read routes check expiry and passwords, with managers bypassing those checks. Password-access routes limit failed attempts by project and client IP, and return content and role after successful access. Tests and documentation cover access behavior, throttling, and version-list gating.
List and revoke shares
backend/geolibre_server_api/geolibre_server_api/main.py, backend/geolibre_server_api/tests/test_shares.py, docs/server-api.md
New endpoints list manageable public and unlisted shares and revoke them by making the project private and clearing its link settings. Tests and documentation cover owner and organization-administrator management.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Reader
  participant PasswordAccessRoute
  participant SharedAccessHandler
  participant AttemptTracker
  participant ProjectRecord
  Reader->>PasswordAccessRoute: Submit project and password
  PasswordAccessRoute->>SharedAccessHandler: Request share access
  SharedAccessHandler->>AttemptTracker: Reserve attempt for project and client IP
  AttemptTracker-->>SharedAccessHandler: Allow attempt or reject with 429
  SharedAccessHandler->>ProjectRecord: Check share settings and retrieve latest content
  ProjectRecord-->>SharedAccessHandler: Return share settings and content
  SharedAccessHandler-->>Reader: Return content and role, or access error
Loading

Merge Risk: ⚪ Minimal · up to 6704a

The reviewed share-management and access paths have no identified issue that needs to be fixed before merge. The documented per-process password throttle remains a deployment limitation.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 6704a

Share protection is centralized and the inspected content routes enforce it before returning data. However, ordinary visibility changes can leave old link restrictions attached to projects, blocking authorized readers outside the link-sharing lifecycle. Password-guessing protection also depends on process-local state and correct proxy configuration. These warrant a design-level review, although no content-access bypass was established.

Retained concerns

  • Medium · reliability · inferred: Link-policy lifetime is not aligned with visibility transitions. Creation rejects password and expiry restrictions for non-link projects, but ordinary visibility PATCH retains existing restrictions. The shared read gate then continues denying non-manager readers after the project becomes private or organization-visible, and the share-revocation endpoint returns 404 rather than clearing that policy. This can strand otherwise authorized organization or group readers behind stale link restrictions and complicate recovery.
Security review details

Security Blast Radius

  • inferred — The new anonymous attack surface targets individual personal or organization project links and their latest stored content. Guessing counters are isolated by project/address/process, so attackers can distribute attempts across addresses, projects, and independently operating processes. No new cross-organization management authority was established.

Security Findings and Attack Paths

  • observed — No retained Security finding was supplied. In the inspected direct-version, raw-content, and unlock paths, expiry/password enforcement precedes content retrieval. This rejects the examined direct-content bypass hypothesis, but does not establish that external storage delivery or unexamined deployment paths are protected.

Trust Boundaries and Controls

  • observed — Caller credentials resolve through persisted authentication records; manager authority is not derived from the route username or password input. Share passwords use the existing salted scrypt helper and constant-time digest comparison.
  • observed — Client-IP derivation uses the peer address unless the peer belongs to configured trusted proxy networks, then evaluates forwarded hops. Application source narrows the spoofing hypothesis, but correct production trust configuration and forwarding-header sanitization remain unverified.

Resilience and Maintainability Implications

  • observed — Attempt reservation occurs under a lock before password verification, preventing simultaneous checks from all passing the same process-local limit. Normal success and handled non-password failures release reservations. Cleanup is not expressed as a finally block, so interruption and unexpected-exception behavior require further validation rather than being treated as a demonstrated attack.

Hardening Proposals

  • proposed — Define one explicit policy-lifecycle rule for visibility changes, transfers, revocation, and republication. Either clear link-only restrictions when leaving link visibility or provide an intentional, authorized recovery path for retained policy.
  • proposed — Make the deployment-wide guessing budget explicit, validate trusted-proxy header handling, and use shared attempt accounting if the security guarantee must survive multiple workers or restarts. Gate protected-share activation and rollback on all serving readers enforcing the persisted policy.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #2803 requires viewing existing shares and an option to update them from local changes. The PR adds and tests GET /api/shares, so the view requirement is implemented. The reviewed changes add … Implement and test the update behavior required by #2803, including the API operation and its local-change handling. If the client already provides the update flow, add reviewable evidence or tests that connect it to this API.
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 3 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding the share API and link role, expiry, and password support.
Description check ✅ Passed The description explains the problem, implementation, testing, deployment impact, and linked issue. It does not use the template's exact Summary and Related issue headings, but it provides the require…
Out of Scope Changes check ✅ Passed The share fields, access checks, throttling, schema upgrades, tests, and documentation support the share listing and management behavior for #2803. The reviewed diff shows no demonstrated unrelated ch…
Full details: Linked Issues check

Explanation

Issue #2803 requires viewing existing shares and an option to update them from local changes. The PR adds and tests GET /api/shares, so the view requirement is implemented. The reviewed changes add listing and revocation, but no update operation or test for updating a share from local changes. The API documentation also lists no such operation.

Full details: Docstring Coverage

Explanation

Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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

A rabbit checks a link at dawn
With role and password neatly drawn
The timer guards each hidden door
And shares can close when needed more
I thump my feet: the links are clear!

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

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://3543802a.geolibre-preview.pages.dev
Demo app https://3543802a.geolibre-preview.pages.dev/demo/
Commit 6704a84

Comment thread backend/geolibre_server_api/geolibre_server_api/main.py Outdated
- Gate the version list with the share link's expiry/password.
- Key the access throttle on the trusted-proxy-aware client address.
- List shares for organization administrators who did not create the project.
- Correct the docs (public/unlisted only, revoke 403/404, 429 throttle).
- Add tests for the version gate, administrator listing, and input validation.
Comment thread backend/geolibre_server_api/geolibre_server_api/main.py Outdated
Comment thread backend/geolibre_server_api/geolibre_server_api/main.py Outdated
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. The expiry/password gating logic (link_gate, visible_read), revoke semantics, and migration paths (Postgres ALTER TABLE / SQLite rebuild) all check out against the test suite and existing helper behavior.

Security

  • share_access_response keys its brute-force throttle on request.client.host instead of the repo's existing client_ip(request) helper (proxy_identity.py), which exists specifically to resolve the real client through a trusted reverse proxy. Behind any GEOLIBRE_TRUSTED_PROXIES deployment, every caller collapses to the proxy's address, turning the "10 attempts per project per client" limit into "10 attempts per project, period" — one bad actor can lock out all real guessers, or distributed attackers are never distinguished. High confidence.
  • The throttle is in-memory and per-process (acknowledged in the docstring), unlike the DB-backed record_failed_login lockout used for account logins elsewhere in auth.py. Under a multi-worker production deployment the effective limit multiplies by worker count. Medium confidence.

Performance

  • The same access_failures dict never evicts keys once their failures age out of the window — only pruned when that exact key is hit again — so it grows unboundedly over a long-running process. Low-medium confidence.

Quality

  • link_gate's datetime.fromisoformat(project.share_expires_at) parses the stored "...Z" timestamp directly, whereas other timestamp parsing in this codebase (auth.py) first does .replace("Z", "+00:00"). Functionally fine given requires-python >= 3.11, just inconsistent with local convention. Low confidence.

CLAUDE.md

  • No violations noted: changes are backend-only (Python), no i18n/UI/Tauri CSP surface touched, and docs/server-api.md was updated alongside the new routes as required.

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-2807/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-2807/demo/
Commit 6704a84

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

Concurrent wrong passwords could all pass the limit check before any failure
was recorded. Reserve the attempt under a lock before checking the password,
release it unless the password was wrong, and prune stale keys.
Managers always read their own projects. Everyone else gets 410 once the
link has expired, and 401 until the project's password is supplied.
"""
if project.share_expires_at is None and project.share_password_hash is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (medium confidence): link_gate enforces expiry/password against every non-manager reader, but role/expiresIn/password can be set on a project of any visibility — not just public/unlisted link shares. ProjectCreate (and create_project) accept these fields regardless of visibility, and validate_access_targets allows visibility: "organization" together with a password.

Concretely: if a project is created with visibility: "organization" and a password, a legitimate organization member who reaches it through the visible() "organization" branch will still be blocked here with 401/410, even though they have real access rights and have no way to know a password that was only ever meant to gate the anonymous public link. The same applies to group-shared private projects.

Consider scoping this gate (and/or the acceptance of role/expiresIn/password at creation) to visibility in {"public", "unlisted"}, since GET/DELETE /api/shares already treat only those visibilities as "link shares".



class ShareAccessRequest(BaseModel):
password: str = Field(max_length=200)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor nit: ShareAccessRequest.password has max_length=200 but no min_length, unlike ProjectCreate.password (min_length=1, max_length=200) a few lines up. Not exploitable (an empty share password can never actually be set, so "" will just never match), but worth aligning for consistency.

Suggested change
password: str = Field(max_length=200)
password: str = Field(min_length=1, max_length=200)

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • link_gate (main.py:1372-1394) enforces share expiry/password against every non-manager reader, but role/expiresIn/password can be attached to a project of any visibility (not just public/unlisted), since ProjectCreate/create_project don't restrict them and validate_access_targets permits visibility: "organization" plus a password. A legitimate organization member or group-share member could be locked out by a 401/410 for a password/expiry they have no way to know, even though GET/DELETE /api/shares treat only public/unlisted as real "link shares." Medium confidence.

Security

  • No issues found. Password hashes are never serialized (only hasPassword boolean), the throttle on wrong-password attempts is applied atomically under a lock before the (expensive) scrypt check, 401 vs other error codes are correctly distinguished so only genuine wrong-password attempts count against the limit, and the new routes consistently reuse visible/visible_read/can_manage_project rather than duplicating authorization logic.

Performance

  • No meaningful issues. The in-memory rate-limit dict has a bounded-cleanup path once it exceeds 1024 entries; the documented per-process limitation is explicitly called out in docs/server-api.md.

Quality

  • ShareAccessRequest.password (main.py:214) has max_length=200 but no min_length=1, unlike the otherwise-identical ProjectCreate.password field a few lines above. Not exploitable (an empty share password can never be set, so "" never matches), but worth aligning for consistency. Low confidence/minor.
  • GET /api/shares (main.py) returns the full unpaginated list of the caller's shares, unlike GET /api/projects which supports limit/offset. Likely fine given expected share counts, but worth noting if a future client reasonably expects pagination. Low confidence.

CLAUDE.md

  • No violations found: the change is confined to the Python FastAPI sidecar (backend/geolibre_server_api), with matching Postgres/SQLite upgrade paths (following the existing delete_protected pattern) and docs/server-api.md updated in the same PR, consistent with repo conventions.

Expiry/password gate every non-manager reader, so on an organization or
private project they would lock out members who cannot know the password.
Return 422 instead, and require a non-empty password on /access.
@deniial00

Copy link
Copy Markdown
Member Author

Addressed the latest review in 6704a84.

  • Link settings on organization/private projects: valid. Expiry and password gate every non-manager reader, so an organization member could be locked out by a password they cannot know. POST /api/projects now returns 422 when password, a non-edit role, or an expiry is sent with a visibility other than public or unlisted. Covered by a new test and documented in docs/server-api.md.
  • ShareAccessRequest.password min length: aligned with ProjectCreate (min_length=1).
  • GET /api/shares pagination: not changed. The list is bounded by one user's shares, and the client reads one unpaginated list. I can add limit/offset if you prefer.
  • Throttle: still in memory and per process, as documented.

account = principal.account
candidates = session.scalars(
select(Project)
.options(*LISTING_EAGER_LOADS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Performance (low confidence, minor): list_shares filters can_manage_project(...) in Python after loading every public/unlisted project the account owns, created, or administers. can_manage_project calls organization_role(...) (a DB query) per project with an organization_id, so this is an N+1 for accounts/orgs with many shares. Also unlike list_projects, there's no limit/offset here, so a prolific org could return an unbounded list. Likely fine at current scale, but worth keeping in mind if this list grows.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • The share link URL users actually copy and send (projectUrl, the /{username}/{slug} and /org/{org}/{slug} page routes) now calls visible_read() with no password, so a password-protected or expired share returns a raw 401/410 instead of the 302 redirect to the viewer — visitors following the link can never reach a password prompt. Medium confidence. (main.py ~line 3539, 3722)
  • ProjectPatch has no fields for role/expiresIn/password and never clears share_role/share_expires_at/share_password_hash when a PATCH changes visibility away from public/unlisted (or moves the project into an organization). This reproduces exactly the lock-out scenario post_project's 422 check is meant to prevent, and DELETE /api/shares/{id} can't clean it up either since it 404s once visibility isn't public/unlisted. Medium-high confidence. (main.py ~line 2908)

Security

  • No issues found. Password hashing (scrypt + constant-time compare), throttling of wrong-password guesses (atomic reserve-before-check, concurrency-tested), and hasPassword-only exposure (no hash leakage) all look correct.

Performance

  • list_shares does an N+1 organization_role lookup per organization-owned candidate project and has no pagination, unlike list_projects. Likely fine at current scale. Low confidence. (main.py ~line 2672)

Quality

  • Logic is clear and well-commented; new tests cover expiry, password, throttling (including concurrency), visibility gating, and organization-admin access well. The .versions[-1] indexing in share_access_response mirrors an existing pattern elsewhere in the file, so not flagged as new.

CLAUDE.md

  • No applicable guidance was violated; backend/geolibre_server_api isn't one of the components CLAUDE.md calls out specifically (it documents backend/geolibre_server), and this PR has no frontend/i18n changes.

@giswqs
giswqs merged commit 8d2b2f5 into opengeos:main Oct 2, 2026
22 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.

[Bug]: Active Shares 404

3 participants