Skip to content

🔗 feat: Add Granular Access Control to Shared Links via ACL System#13051

Merged
danny-avila merged 19 commits into
devfrom
feat/shared-links-access-control
Jun 3, 2026
Merged

🔗 feat: Add Granular Access Control to Shared Links via ACL System#13051
danny-avila merged 19 commits into
devfrom
feat/shared-links-access-control

Conversation

@AtefBellaaj

@AtefBellaaj AtefBellaaj commented May 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds granular access control to shared links by integrating them into the existing ACL permission system. Previously, shared links were globally visible with no per-link access control (a boolean isPublic field, always true). This feature lets users share conversation links with specific users, groups, roles, or everyone — using the same principal system as agents, prompts, and skills.

Key behaviors:

  • By default, both USER and ADMIN roles have CREATE: true, SHARE: true, SHARE_PUBLIC: true — all sharing features are enabled out of the box. Admins can restrict permissions per-role via librechat.yaml or the admin panel: CREATE gates link creation, SHARE gates the "Manage Access" UI, and SHARE_PUBLIC controls whether links are created public by default and whether users can toggle the "shared with everyone" switch.
  • Creating a shared link auto-grants OWNER to the creator and PUBLIC VIEWER when the user's role has SHARE_PUBLIC: true (preserving current default-public behavior). When SHARE_PUBLIC: false, links are created private — the creator must explicitly share via "Manage Access." Both grants roll back on failure.
  • Legacy links (pre-migration) auto-migrate on first visit by creating AclEntry rows on the fly. Owner lookup (GET /link/:conversationId) also ensures OWNER permission exists, so the "Manage Access" button appears immediately without requiring a public visit first. A bulk migration script is also available for pre-migration.
  • The isPublic boolean field is removed from SharedLink documents — "shared with everyone" is now derived from the presence of a PUBLIC AclEntry.
  • OWNER role is protected: cannot be assigned to non-owners, demoted, or removed via the permission API.

Important notes:

Tenant Context in Shared Link Access

Shared links require special tenant handling because they support anonymous/public access.

Normal routes (prompts, agents): requireJwtAuth → tenantContextMiddleware sets tenant from user's JWT. All downstream queries are automatically tenant-scoped.

Shared link viewing: Anonymous users have no JWT, so no tenant context exists. But:

  1. The SharedLink document has tenantId (set at creation from owner's context)
  2. The AclEntry records have tenantId (same tenant as the resource)
  3. In strict mode (TENANT_ISOLATION_STRICT=true), queries without tenant context throw

Solution: After fetching the SharedLink (cross-tenant lookup by shareId), we restore tenant context from the document via tenantStorage.run({ tenantId: rawShare.tenantId }). This ensures AclEntry permission checks are correctly tenant-scoped.

For authenticated users, optionalJwtAuth already sets tenant context from their JWT — but since cross-tenant sharing is prevented upstream (people picker is tenant-filtered), using the document's tenantId is equivalent and maintains consistency.

Change Type

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Testing

Automated tests

All test suites pass with zero regressions:

  • packages/data-provider: 20/20 suites, 1083 tests passing
  • @librechat/backend: 131/134 suites, 2484 tests passing (3 pre-existing failures unrelated)
  • @librechat/api: 184/205 suites, 5109 tests passing (20 pre-existing failures — Redis cache integration, worker limits)

New test files:

  • packages/api/src/share/service.test.ts: 10 tests — grantCreationPermissions (happy path, rollback), ensureOwnerPermission (creation, idempotency, no-rollback), delete-with-cleanup variants
  • packages/api/src/share/access.test.ts: 11 tests — resolveShareAccess input validation, public links (anonymous + authenticated), private links (401/403/200), legacy auto-migration (public/private/disabled)

Test areas covered

  • Share method tests (packages/data-schemas/src/methods/share.test.ts): Updated 42 tests for isPublic removal, added _id return assertions
  • Permission propagation (packages/api/src/app/permissions.spec.ts): All 41 tests pass with SHARED_LINKS permission type
  • ACL access resolver (packages/api/src/share/access.test.ts): 11 tests covering 8+ security branches
  • ACL service lifecycle (packages/api/src/share/service.test.ts): 10 tests covering creation grants, rollback, and cleanup
  • User deletion coverage (deleteUserResourceCoverage.spec.js): SHARED_LINK resource type has documented cleanup handler
  • Role defaults (packages/data-provider/src/roles.spec.ts): SHARED_LINKS tracked with CREATE, SHARE, SHARE_PUBLIC
  • Convos route tests (convos.spec.js): Updated to use deleteAllSharedLinksWithCleanup and deleteConvoSharedLinksWithCleanup

Manual testing checklist

  • Create a shared link → verify OWNER + PUBLIC VIEWER AclEntries created (when SHARE_PUBLIC: true)
  • Create a shared link with SHARE_PUBLIC: false → verify only OWNER AclEntry created (private link)
  • With SHARE: true, verify "Manage Access" button appears on existing link
  • Toggle "Shared with everyone" off → verify link requires authentication and specific principal access
  • Add specific users via People Picker → verify they can access the link
  • Verify OWNER principal shows static "Owner" label (not a role dropdown) and no remove button
  • Delete a shared link → verify AclEntries cleaned up
  • Delete all conversations → verify shared link AclEntries cleaned up
  • Delete a user account → verify all their shared link AclEntries cleaned up
  • Visit a legacy link (pre-migration) → verify auto-migration creates AclEntries transparently
  • Set SHARED_LINKS_AUTO_MIGRATE=false → verify legacy links return 403 until bulk migration runs
  • Run node config/migrate-shared-link-permissions.js --dry-run → verify report without writes
  • Run bulk migration → verify isPublic field $unset from all SharedLink documents
  • With CREATE: false, verify share button is hidden in conversation menu and chat header

Test Configuration

  • Node.js v20.19.0+
  • MongoDB (any version supporting the existing schema)
  • ALLOW_SHARED_LINKS_PUBLIC: controls whether "shared with everyone" links allow anonymous access
  • SHARED_LINKS_AUTO_MIGRATE: controls auto-migration of legacy links (default: true)

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@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: d785e7ebfc

ℹ️ 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 thread config/migrate-shared-link-permissions.js Outdated
Comment thread api/server/services/PermissionService.js Outdated
@danny-avila

Copy link
Copy Markdown
Owner

Note, this is relevant: #13062

There should also be a permission gate

@AtefBellaaj AtefBellaaj force-pushed the feat/shared-links-access-control branch from d785e7e to bd494fc Compare May 11, 2026 20:44
@AtefBellaaj

Copy link
Copy Markdown
Collaborator Author

@codex review

@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

await db.deleteAllSharedLinks(req.user.id);

P2 Badge Clean up shared-link ACLs on delete-all conversations

The /api/convos/all path still calls db.deleteAllSharedLinks(req.user.id) instead of the new cleanup helper that also removes ACL entries. After deleting all conversations, shared-link documents are removed but their ACL rows remain orphaned, causing permission data drift and accumulating stale ACL records over time.

ℹ️ 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 thread client/src/components/Sharing/GenericGrantAccessDialog.tsx Outdated
@AtefBellaaj

Copy link
Copy Markdown
Collaborator Author

@codex please review all changes again, because of force push.

@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: 880d63d9ee

ℹ️ 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 thread client/src/components/Sharing/GenericGrantAccessDialog.tsx Outdated
Comment thread api/server/routes/convos.js
@atefbellaaj-at-slalom atefbellaaj-at-slalom linked an issue May 12, 2026 that may be closed by this pull request
1 task
@AtefBellaaj

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 8e3e1996e1

ℹ️ 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 thread api/server/services/PermissionService.js Outdated
Comment on lines +446 to +448
const share = (await SharedLink.findOne({ conversationId, user })
.select('shareId _id')
.lean()) as { shareId?: string; _id?: import('mongoose').Types.ObjectId } | null;

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 Migrate legacy shared-link ACL on owner lookup

getSharedLink() now returns the shared link _id, which the UI immediately uses to compute ACL bits before showing “Manage Access”, but this path never runs the legacy ACL migration. For pre-migration links (no AclEntry rows yet), the owner receives zero effective permissions and the manage-access control stays hidden until someone visits the public share URL first. Triggering the same legacy-migration logic during owner lookup avoids this regression for existing links.

Useful? React with 👍 / 👎.

@AtefBellaaj AtefBellaaj force-pushed the feat/shared-links-access-control branch 2 times, most recently from e00dd80 to 07bcffd Compare May 12, 2026 20:59
@AtefBellaaj

Copy link
Copy Markdown
Collaborator Author

@codex review

@AtefBellaaj AtefBellaaj force-pushed the feat/shared-links-access-control branch from 07bcffd to 53a53c0 Compare May 12, 2026 21:05

@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: 07bcffd3e6

ℹ️ 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 thread config/migrate-shared-link-permissions.js Outdated
Comment thread api/server/services/PermissionService.js Outdated
@AtefBellaaj AtefBellaaj force-pushed the feat/shared-links-access-control branch 3 times, most recently from d1513e8 to 7878f50 Compare May 12, 2026 22:00
@AtefBellaaj

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 7878f502fb

ℹ️ 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 thread api/server/services/PermissionService.js Outdated
@AtefBellaaj AtefBellaaj force-pushed the feat/shared-links-access-control branch 3 times, most recently from ac0cac1 to 278cec0 Compare May 13, 2026 15:53
@AtefBellaaj

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 278cec0fc7

ℹ️ 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 thread packages/api/src/shared-links/service.ts
Comment thread packages/api/src/app/permissions.ts
@AtefBellaaj AtefBellaaj force-pushed the feat/shared-links-access-control branch from 278cec0 to f2275a8 Compare May 13, 2026 16:18
@AtefBellaaj

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: f2275a8d6d

ℹ️ 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 thread api/server/routes/share.js
Comment thread packages/api/src/app/permissions.ts Outdated
@AtefBellaaj AtefBellaaj force-pushed the feat/shared-links-access-control branch from f2275a8 to 462b651 Compare May 14, 2026 10:24
@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@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: 4968fdf91c

ℹ️ 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 thread packages/api/src/shared-links/access.ts Outdated
}

const SharedLink = mg.models.SharedLink as Model<RawSharedLink>;
const rawShare = (await SharedLink.findOne({ shareId }).lean()) as RawSharedLink | null;

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 Run public share lookup in system tenant context

In deployments with TENANT_ISOLATION_STRICT=true, anonymous public-link requests reach this middleware without a tenant in ALS, but SharedLink has the tenant-isolation plugin applied (packages/data-schemas/src/models/sharedLink.ts) and its pre('findOne') hook throws when no tenant context exists. Because the first lookup runs before runWithTenant, public shared links return a 500 instead of loading; perform this shareId lookup in an explicit system/cross-tenant context or via a raw collection query before restoring rawShare.tenantId for ACL checks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Fixed in c1ec4b5: the initial shared-link lookup now runs under the request tenant when present, or under runAsSystem when no tenant context exists, and no-tenant legacy rows continue under system context for ACL/message checks.

Comment on lines +85 to +90
} else if (resourceType === ResourceType.SHARED_LINK) {
middleware = canAccessResource({
resourceType: ResourceType.SHARED_LINK,
requiredPermission,
resourceIdParam: 'resourceId',
});

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 Enforce shared-link owner immutability server-side

Adding sharedLink to the generic permissions route exposes the existing PUT /api/permissions/:resourceType/:resourceId updater for shared links, but the route only verifies the caller currently has SHARE and then passes arbitrary updated/removed principals to bulkUpdateResourcePermissions. For shared links, a caller with owner permissions can still demote/remove the owner or assign sharedLink_owner to another principal by calling the API directly, despite the new shared-link flow relying on OWNER being protected; add a server-side guard for ResourceType.SHARED_LINK before delegating to the generic updater.

Useful? React with 👍 / 👎.

Comment on lines +27 to +30
const checkSharedLinksAccess = generateCheckAccess({
permissionType: PermissionTypes.SHARED_LINKS,
permissions: [Permissions.CREATE],
getRoleByName,

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 Don’t require CREATE to delete existing links

This middleware is reused for PATCH and DELETE below, so when an admin disables sharedLinks.create to stop new link creation, users are also blocked from refreshing or deleting shared links they already own even though the route methods still enforce ownership by shareId and user. That leaves migrated or previously created links stuck until the role is allowed to create new links again; gate only POST on CREATE, and use the appropriate ownership/share check for modifying existing links.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Fixed in c1ec4b5: CREATE role gating remains on POST only; PATCH and DELETE now rely on authenticated ownership/resource checks so existing links can still be refreshed or deleted after new creation is disabled.

}
}
}
const needsPublicGrant = share.isPublic !== false && !!share.user;

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 Preserve public access for ownerless legacy links

For legacy SharedLink documents that have isPublic: true but no user field (the schema allows this and the bulk migration explicitly counts them as missingUserWarnings), this condition skips the PUBLIC grant and the function later unsets isPublic, so the first anonymous visit permanently converts a formerly public link into one that returns 401/403. The public viewer grant should depend on the legacy public flag, not on whether an owner can also be granted.

Useful? React with 👍 / 👎.

}

// --- Count totals for progress reporting ---
const totalLinks = await SharedLink.countDocuments({});

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 Run the shared-link migration as a system tenant

With TENANT_ISOLATION_STRICT=true, SharedLink has tenant-isolation query middleware, so this cross-tenant migration’s first model call without ALS tenant context throws before the dry run or live migration can inspect any rows. Since the script is intended to scan all tenants, wrap the Mongoose model operations in runAsSystem or use the raw collection consistently.

Useful? React with 👍 / 👎.

Comment thread api/server/routes/share.js Outdated
Comment on lines +113 to +114
if (share._id && share.success) {
ensureLinkPermissions(share._id, req.user.id).catch(() => {});

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 Await owner backfill before returning the link

This fire-and-forget backfill races the UI path that immediately calls /api/permissions/sharedLink/:id/effective to decide whether to show Manage Access. For legacy links without an OWNER ACL yet, the response can reach the client before ensureLinkPermissions has inserted the ACL entry, so the effective-permissions query returns 0 and the Manage Access button stays hidden until a later refetch/reopen; await the backfill (or create it inside getSharedLink) before sending the _id response.

Useful? React with 👍 / 👎.

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@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: aa9742de6c

ℹ️ 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 +115 to +117
const grantsOwner = updated.some(
(principal) => principal?.accessRoleId === AccessRoleIds.SHARED_LINK_OWNER,
);

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 owner role for public shared-link grants

This guard only checks updated, but the controller later turns public: true plus publicAccessRoleId into a PUBLIC ACL entry. If a caller sends publicAccessRoleId: sharedLink_owner, the public principal gets owner bits, and because ACL checks include the PUBLIC principal for authenticated users, other users can satisfy SHARE and manage this shared link's permissions. Please also reject req.body.publicAccessRoleId === AccessRoleIds.SHARED_LINK_OWNER for shared links.

Useful? React with 👍 / 👎.

Comment on lines +30 to +34
{canCreateSharedLinks && (
<div className="pb-3">
<SharedLinks />
</div>
)}

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 Keep existing shared links visible without CREATE

Gating the entire Shared Links settings panel on CREATE means that when an admin revokes shared-link creation from a role, users who already have shared links lose the UI to view or delete those existing links. The server-side delete route was explicitly left outside the CREATE gate, so this leaves stale links active unless the user calls the API directly; the list/delete panel should remain accessible when shared links exist even if creating new ones is disabled.

Useful? React with 👍 / 👎.

Comment on lines +57 to +58
const isPublicFalseCount = await rawCollection.countDocuments({ isPublic: false });
if (isPublicFalseCount > 0 && !force) {

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 Allow dry-runs with private legacy links

Because this abort check runs before the dryRun branch and ignores dryRun, npm run migrate:shared-link-permissions:dry-run exits as aborted whenever any legacy isPublic: false link exists. That prevents operators from using the dry-run report below to assess how many private links they have before deciding whether to run with --force; the abort should only block the live migration path.

Useful? React with 👍 / 👎.

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@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: 2b99a27a70

ℹ️ 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 +396 to +399
const publicIds: Types.ObjectId[] = await this._dbMethods.findPublicResourceIds(
resourceType,
PermissionBits.VIEW,
);

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 Avoid loading every public shared-link ACL

When /api/share/:shareId checks a public shared link, this call materializes every public resource ID for the resource type before the caller scans for the one requested link. On installations with many public shared links, each share view becomes O(all public links) and allocates a large array on the hot path; query for the single (resourceType, resourceId, PUBLIC, VIEW) ACL entry instead.

Useful? React with 👍 / 👎.

[ResourceType.MCPSERVER]: PermissionTypes.MCP_SERVERS,
[ResourceType.REMOTE_AGENT]: PermissionTypes.REMOTE_AGENTS,
[ResourceType.SKILL]: PermissionTypes.SKILLS,
[ResourceType.SHARED_LINK]: PermissionTypes.SHARED_LINKS,

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 Enforce SHARE_PUBLIC for public-principal grants

With shared links added to the generic share policy, a user who has SHARE but not SHARE_PUBLIC can still add a PUBLIC viewer by sending it in updated and omitting public; checkSharePublicAccess only gates req.body.public, while updateResourcePermissions grants PUBLIC principals from updated. This bypasses the new shared-link public-sharing restriction whenever admins disable SHARE_PUBLIC, so PUBLIC entries in updated need the same gate or should be rejected.

Useful? React with 👍 / 👎.

Comment thread api/server/routes/share.js Outdated
Comment on lines +146 to +149
expiredAt,
);
if (created) {
await grantCreationPermissions(created._id, req.user.id, grantPublic);

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 Clean up ACLs when expiring shared links

When this route creates a retained share with expiredAt, the SharedLink document is later removed by the schema TTL index, but the ACL entries granted here have no matching TTL or delete hook. Those orphan USER/PUBLIC ACLs accumulate and continue to appear in permission scans after the link is gone; either expire the ACL entries with the link or run cleanup when TTL-based deletion occurs.

Useful? React with 👍 / 👎.

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

ℹ️ 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".

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

ℹ️ 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".

@danny-avila danny-avila changed the title 🛡️ feat: Shared Links Granular Access Control via ACL System 🔗 feat: Add Granular Access Control to Shared Links via ACL System Jun 3, 2026
@danny-avila danny-avila merged commit 86fe79c into dev Jun 3, 2026
19 checks passed
@danny-avila danny-avila deleted the feat/shared-links-access-control branch June 3, 2026 18:17
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request Jun 4, 2026
…anny-avila#13051)

* feat: Add granular access control to shared links via ACL system

* fix(shared-links): preserve isPublic on failed migration grants

Transient ACL failures during auto-migration permanently stranded
links — $unset ran unconditionally, removing the legacy flag that
triggers retry. Now only $unset isPublic after all grants succeed.

* fix(config): skip isPublic unset for failed ACL grants

Bulk migration unconditionally removed isPublic from all links,
even those whose ACL writes failed. Failed links then lost the
legacy marker needed for auto-migration retry. Now tracks failed
link IDs per-batch and excludes them from the $unset step.

Also adds sharedLink to AccessRole resourceType schema enum —
was missing, only worked because seedDefaultRoles uses
findOneAndUpdate which bypasses validation.

* ci(config): add jest config and PR workflow for migration tests

config/__tests__/ specs depend on api/jest.config.js module
mappings but had no dedicated runner. Adds config/jest.config.js
extending api config with absolutized paths, npm test:config
script, and a GitHub Actions workflow triggered by changes to
config/, api/models/, api/db/, or packages/ ACL code.

* fix(permissions): honor boolean sharedLinks config

SHARED_LINKS has no USE permission, so boolean config produced
an empty update payload — gate conditions only matched object
form, making `sharedLinks: false` a no-op on existing perms.

* fix(share): resolve role before creating shared link

Role lookup between create and grant left an orphaned link
without ACL entries if getRoleByName threw — retry then hit "Share already exists" with no recovery path.

* fix: Restore Public ACL Access Checks

* fix: Type Public ACL Lookup

* fix: Preserve Private Legacy Shared Links

* chore: Promote Shared Link Permission Migration

* fix: Address Shared Link Review Findings

* fix: Repair Shared Link CI Follow-Up

* fix: Narrow Shared Link Mongoose Test Mock

* fix: Address Shared Link Review Follow-Ups

* fix: Close Shared Link Review Gaps

* fix: Guard Missing Shared Link Permission Backfill

* test: Add Shared Link Mock E2E

* test: Stabilize Shared Link Mock E2E

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
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.

[Enhancement]: Add Granular access control to Shared Links

2 participants