mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-27 10:05:32 +02:00
feat(preview): add collaborator permission requirement for preview deployments
- Introduced a new boolean field `previewRequireCollaboratorPermissions` in the application schema to enforce permission checks for preview deployments. - Updated the UI to include a toggle for this setting in the preview deployment settings. - Enhanced GitHub deployment handler to validate PR authors against the required permissions, blocking unauthorized deployments and providing security notifications. - Added SQL migration to update the database schema accordingly.
This commit is contained in:
@@ -46,6 +46,7 @@ const schema = z
|
||||
previewPath: z.string(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||
previewCustomCertResolver: z.string().optional(),
|
||||
previewRequireCollaboratorPermissions: z.boolean(),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (
|
||||
@@ -83,6 +84,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
previewHttps: false,
|
||||
previewPath: "/",
|
||||
previewCertificateType: "none",
|
||||
previewRequireCollaboratorPermissions: true,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -105,6 +107,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
previewPath: data.previewPath || "/",
|
||||
previewCertificateType: data.previewCertificateType || "none",
|
||||
previewCustomCertResolver: data.previewCustomCertResolver || "",
|
||||
previewRequireCollaboratorPermissions:
|
||||
data.previewRequireCollaboratorPermissions || true,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
@@ -121,6 +125,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
previewPath: formData.previewPath,
|
||||
previewCertificateType: formData.previewCertificateType,
|
||||
previewCustomCertResolver: formData.previewCustomCertResolver,
|
||||
previewRequireCollaboratorPermissions:
|
||||
formData.previewRequireCollaboratorPermissions,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Preview Deployments settings updated");
|
||||
@@ -312,6 +318,32 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewRequireCollaboratorPermissions"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
Require Collaborator Permissions
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Require collaborator permissions to preview
|
||||
deployments.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="env"
|
||||
|
||||
1
apps/dokploy/drizzle/0103_cultured_pestilence.sql
Normal file
1
apps/dokploy/drizzle/0103_cultured_pestilence.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "previewRequireCollaboratorPermissions" boolean DEFAULT true;
|
||||
6136
apps/dokploy/drizzle/meta/0103_snapshot.json
Normal file
6136
apps/dokploy/drizzle/meta/0103_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -722,6 +722,13 @@
|
||||
"when": 1751848685503,
|
||||
"tag": "0102_opposite_grandmaster",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 103,
|
||||
"version": "7",
|
||||
"when": 1752465764072,
|
||||
"tag": "0103_cultured_pestilence",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
findPreviewDeploymentsByPullRequestId,
|
||||
removePreviewDeployment,
|
||||
shouldDeploy,
|
||||
findGithubById,
|
||||
checkUserRepositoryPermissions,
|
||||
createSecurityBlockedComment,
|
||||
} from "@dokploy/server";
|
||||
import { Webhooks } from "@octokit/webhooks";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
@@ -346,6 +349,18 @@ export default async function handler(
|
||||
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||
const branch = githubBody?.pull_request?.base?.ref;
|
||||
const owner = githubBody?.repository?.owner?.login;
|
||||
const prAuthor = githubBody?.pull_request?.user?.login;
|
||||
|
||||
// Validate PR author information is present
|
||||
if (!prAuthor) {
|
||||
console.warn(
|
||||
"⚠️ SECURITY: PR author information missing in webhook payload",
|
||||
);
|
||||
res.status(400).json({
|
||||
message: "PR author information missing",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const apps = await db.query.applications.findMany({
|
||||
where: and(
|
||||
@@ -361,15 +376,74 @@ export default async function handler(
|
||||
},
|
||||
});
|
||||
|
||||
// SECURITY: Check collaborator permissions per application setting
|
||||
const secureApps: typeof apps = [];
|
||||
const blockedApps: string[] = [];
|
||||
let userPermission: string | null = null;
|
||||
|
||||
for (const app of apps) {
|
||||
// If the app requires collaborator permissions, verify them
|
||||
if (app.previewRequireCollaboratorPermissions !== false) {
|
||||
try {
|
||||
const githubProvider = await findGithubById(githubResult.githubId);
|
||||
const { hasWriteAccess, permission } =
|
||||
await checkUserRepositoryPermissions(
|
||||
githubProvider,
|
||||
owner,
|
||||
repository,
|
||||
prAuthor,
|
||||
);
|
||||
|
||||
userPermission = permission; // Store permission for comment
|
||||
|
||||
if (!hasWriteAccess) {
|
||||
console.warn(
|
||||
`🚨 SECURITY: Blocked preview deployment for ${app.name} from unauthorized user ${prAuthor} on ${owner}/${repository}. Permission: ${permission || "none"}`,
|
||||
);
|
||||
blockedApps.push(app.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ SECURITY: Preview deployment authorized for ${app.name} from user ${prAuthor} on ${owner}/${repository}. Permission: ${permission}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error validating PR author permissions for ${app.name}:`,
|
||||
error,
|
||||
);
|
||||
blockedApps.push(app.name);
|
||||
continue; // Skip this app on error
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`⚠️ SECURITY: Preview deployment for ${app.name} allows deployment from any PR author (security check disabled)`,
|
||||
);
|
||||
}
|
||||
secureApps.push(app);
|
||||
}
|
||||
|
||||
const prBranch = githubBody?.pull_request?.head?.ref;
|
||||
|
||||
const prNumber = githubBody?.pull_request?.number;
|
||||
const prTitle = githubBody?.pull_request?.title;
|
||||
const prURL = githubBody?.pull_request?.html_url;
|
||||
|
||||
for (const app of apps) {
|
||||
// Create security notification comment if any apps were blocked
|
||||
if (blockedApps.length > 0) {
|
||||
await createSecurityBlockedComment({
|
||||
owner,
|
||||
repository,
|
||||
prNumber: Number.parseInt(prNumber),
|
||||
prAuthor,
|
||||
permission: userPermission,
|
||||
githubId: githubResult.githubId,
|
||||
});
|
||||
}
|
||||
|
||||
for (const app of secureApps) {
|
||||
const previewLimit = app?.previewLimit || 0;
|
||||
if (app?.previewDeployments?.length > previewLimit) {
|
||||
if (app?.previewDeployments?.length >= previewLimit) {
|
||||
continue;
|
||||
}
|
||||
const previewDeploymentResult =
|
||||
|
||||
Reference in New Issue
Block a user