feat: add per-member server access control#3933
Open
lcaputo wants to merge 8 commits intoDokploy:canaryfrom
Open
feat: add per-member server access control#3933lcaputo wants to merge 8 commits intoDokploy:canaryfrom
lcaputo wants to merge 8 commits intoDokploy:canaryfrom
Conversation
apps/dokploy/components/dashboard/settings/users/server-permissions-section.tsx
Outdated
Show resolved
Hide resolved
- Removed the updateMemberServers - Global press animation restored to buttonVariants. The combobox button in add-permissions.tsx:921 already has active:scale-100 active:translate-y-0 which overrides the global styles specifically for that button - Refactor checkServiceAccess to fetch the member once internally and return it - Make canAccessServer accept an optional pre-fetched member - Update the 8 "create" call sites in routers to pass the member through
…in add-permissions
…uild server access filter
Author
|
@greptile review |
…s helper in server router
Author
|
@greptile review |
… from canUseLocalServer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this PR about?
This PR introduces server-level access control for members.
Changes
Adds
accessedServersfield to the member permissions modelIntegrates server selection as a multi-select dropdown in the permissions form
Enforces
canAccessServerguards on all service creation routes:Tested locally: members without server access are blocked from creating services on restricted servers. Multi-select correctly persists and loads server selections.
Checklist
Before submitting this PR, please make sure that:
canarybranch.Issues related (if applicable)
Screenshots (if applicable)
Greptile Summary
This PR adds per-member server access control to Dokploy. It introduces an
accessedServersfield to the member schema (with migration), acanAccessServerservice function, server filtering on API endpoints, andcanAccessServerguards on all service creation routes. The permissions UI is extended with a multi-select combobox to assign server access to members.The backend security enforcement is solid. However, two UI-layer issues remain:
!isCloudundefined guard in permissions form: WhenisCloudis still loading (undefined),!isCloudevaluates totrue, temporarily including "local" inallServerIdsand rendering the "Dokploy" option even on cloud deployments. An admin who clicks "Select All" before the query resolves would inadvertently save "local" into the member'saccessedServers. Fix: useisCloud === falseinstead of!isCloud.canUseLocalServerflash during loading: Inadd-application.tsx,add-compose.tsx,add-database.tsx,add-template.tsx, andai/step-one.tsx, the "Dokploy" option briefly appears for members without local access due toisLoadingMemberbeing the first condition. Since!currentMemberalready covers the loading case, removingisLoadingMember ||eliminates the flash entirely.Confidence Score: 3/5
!isCloudundefined guard in the permissions form that could temporarily include "local" as an option on cloud deployments. The!isCloudissue is more serious as it affects cloud deployments and could persist data, warranting a fix before deploying to cloud instances.Comments Outside Diff (11)
apps/dokploy/components/dashboard/project/add-application.tsx, line 212-215 (link)defaultValuerace condition for restricted memberscurrentMemberisundefinedwhileapi.user.getis still loading, which makescanUseLocalServer = trueon the first render. For a member without local-server access, this meansdefaultValue="dokploy"is passed to<Select>on mount. Because Radix UI'sSelecttreatsdefaultValueas an uncontrolled initial state, the component retains "Dokploy" as its displayed selection even after the query resolves and the<SelectItem value="dokploy">is removed from the DOM.The actual
field.valueremainsundefined(sincedefaultValuenever firesonChange), so a form submission sendsserverId: undefinedwhich the backend resolves as"local"and then correctly rejects — but the user sees a confusing error rather than a blocked submit button.The same pattern is repeated in
add-compose.tsx,add-database.tsx,add-template.tsx, andai/step-one.tsx.A minimal fix is to defer setting
defaultValueuntil the query has settled:This way, while
currentMemberis still loading the local-server option is simply absent (no spurious pre-selection), and once the data arrives the correct value is applied.apps/dokploy/server/api/routers/server.ts, line 131-158 (link)buildServersnot filtered byaccessedServersserver.all(line 90) andserver.withSSHKey(line 131) now both filter results byaccessedServersfor members, butserver.buildServers(line 159) returns all build servers without any such filtering. If a member has no access to a given build server they should still not be able to see or select it during service creation. Unless build-server assignment is intentionally admin-only and members never interact with this list, the same guard should be applied here:apps/dokploy/components/dashboard/settings/users/add-permissions.tsx, line 935-939 (link)Server badge removal breaks with duplicate server names
The badge
Xbutton resolves a server'sserverIdby reverse-searchingallServersby name:Two issues here:
Array.findreturns the first match — clickingXon the second server's badge silently removes the first server's ID instead.findreturnsundefined),idbecomes""andtoggleServer("")will append the empty string toaccessedServersrather than removing the intended entry.Consider building
allLabelsas{ label: string; id: string }[]so theidis captured at badge-render time rather than re-derived from the display name on click. This avoids the reverse-lookup entirely.apps/dokploy/components/dashboard/project/add-application.tsx, line 79-83 (link)Loading state causes Select default to initialize as undefined
canUseLocalServerevaluates tofalsewhileisLoadingMemberistruebecause of the leading!isLoadingMember &&guard. Since Radix UI'sSelectreadsdefaultValueonly on initial mount, when the member query resolves andcanUseLocalServerflips totrue, thedefaultValueprop change will not retroactively update the already-mounted component. This means owners and admins on self-hosted (non-cloud) installations will see the "Select a Server" placeholder instead of "Dokploy" being pre-selected — a regression from the pre-PR behaviour.The correct fix is to be optimistic during the loading state (the server-side guard is the real enforcement):
This same pattern is repeated in:
add-compose.tsx(lines 80–84)add-database.tsx(lines 186–190)add-template.tsx(lines 121–125)ai/step-one.tsx(lines 31–35)All five files need this update.
apps/dokploy/server/api/routers/server.ts, line 112-117 (link)Redundant
findMemberByIdcalls across server listing queriesall,withSSHKey, andbuildServerseach independently callfindMemberByIdfor non-admin users (lines 112–117, 152–157, 180–185). When a page load triggers multiple of these queries in parallel—for example, inapps/dokploy/components/dashboard/settings/cluster/registry/handle-registry.tsxwhich calls bothwithSSHKeyandbuildServerson mount—this results in duplicate round-trips to fetch the same member row.The same redundancy was already identified and fixed for the service creation routers (where
checkServiceAccessnow returns the member, which is then passed tocanAccessServer). The same fix should be applied here—either by sharing a singlefindMemberByIdcall across these three handlers, or by pulling the filtering into a shared helper that accepts an optional pre-fetched member.Example refactor approach:
Apply the same pattern to
withSSHKey(lines 152–157) andbuildServers(lines 180–185).apps/dokploy/components/dashboard/settings/users/add-permissions.tsx, line 885-893 (link)!isCloudevaluates totruewhile the query is loading (whenisCloudisundefined). This means "local" is temporarily included inallServerIdson cloud deployments. If an admin clicks "Select All" before the query resolves, "local" persists in the member'saccessedServers.The same issue appears at line 968 with the
{!isCloud && (conditional that renders the "Dokploy" option.Fix by using
isCloud === falseto treat the loading state as cloud-safe by default:Also update line 968:
apps/dokploy/components/dashboard/project/add-application.tsx, line 79-83 (link)The "Dokploy" option briefly flashes during the initial member data load.
canUseLocalServerevaluates totruewhenisLoadingMemberis true due to the first OR condition, even for members without local access. When loading completes, thekeyprop triggers a remount and the option disappears.Since
!currentMemberalready handles the undefined case,isLoadingMemberis redundant. Removing it eliminates the flash:The same pattern applies in
add-compose.tsx,add-database.tsx,add-template.tsx, andai/step-one.tsx.apps/dokploy/components/dashboard/project/add-compose.tsx, line 80-84 (link)Same issue as in
add-application.tsx: remove the redundantisLoadingMember ||condition.apps/dokploy/components/dashboard/project/add-database.tsx, line 186-190 (link)Same issue as in
add-application.tsx: remove the redundantisLoadingMember ||condition.apps/dokploy/components/dashboard/project/add-template.tsx, line 121-125 (link)Same issue as in
add-application.tsx: remove the redundantisLoadingMember ||condition.apps/dokploy/components/dashboard/project/ai/step-one.tsx, line 31-35 (link)Same issue as in
add-application.tsx: remove the redundantisLoadingMember ||condition.Last reviewed commit: 749d990
Context used: