fix(webhook): skip preview deployments for fork PRs when public previews are off
This commit is contained in:
parent
dd8a0d501d
commit
ab4b2045d4
3 changed files with 51 additions and 2 deletions
|
|
@ -62,6 +62,7 @@ public function manual(Request $request)
|
||||||
$before_sha = data_get($payload, 'before');
|
$before_sha = data_get($payload, 'before');
|
||||||
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
||||||
$author_association = data_get($payload, 'pull_request.author_association');
|
$author_association = data_get($payload, 'pull_request.author_association');
|
||||||
|
$is_fork_pull_request = $this->isForkPullRequest($payload);
|
||||||
}
|
}
|
||||||
if (! in_array($x_github_event, ['push', 'pull_request'])) {
|
if (! in_array($x_github_event, ['push', 'pull_request'])) {
|
||||||
return response("Nothing to do. Event '$x_github_event' is not supported.");
|
return response("Nothing to do. Event '$x_github_event' is not supported.");
|
||||||
|
|
@ -222,6 +223,7 @@ public function manual(Request $request)
|
||||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||||
authorAssociation: $author_association,
|
authorAssociation: $author_association,
|
||||||
fullName: $full_name,
|
fullName: $full_name,
|
||||||
|
isForkPullRequest: $is_fork_pull_request ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
$return_payloads->push([
|
$return_payloads->push([
|
||||||
|
|
@ -303,6 +305,7 @@ public function normal(Request $request)
|
||||||
$before_sha = data_get($payload, 'before');
|
$before_sha = data_get($payload, 'before');
|
||||||
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
||||||
$author_association = data_get($payload, 'pull_request.author_association');
|
$author_association = data_get($payload, 'pull_request.author_association');
|
||||||
|
$is_fork_pull_request = $this->isForkPullRequest($payload);
|
||||||
}
|
}
|
||||||
if (! in_array($x_github_event, ['push', 'pull_request'])) {
|
if (! in_array($x_github_event, ['push', 'pull_request'])) {
|
||||||
return response("Nothing to do. Event '$x_github_event' is not supported.");
|
return response("Nothing to do. Event '$x_github_event' is not supported.");
|
||||||
|
|
@ -434,6 +437,7 @@ public function normal(Request $request)
|
||||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||||
authorAssociation: $author_association,
|
authorAssociation: $author_association,
|
||||||
fullName: $full_name,
|
fullName: $full_name,
|
||||||
|
isForkPullRequest: $is_fork_pull_request ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
$return_payloads->push([
|
$return_payloads->push([
|
||||||
|
|
@ -451,6 +455,40 @@ public function normal(Request $request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a pull_request webhook payload originates from a fork.
|
||||||
|
*
|
||||||
|
* GitHub's `author_association` is not a reliable trust signal (it grants
|
||||||
|
* CONTRIBUTOR to anyone who has merely opened an issue/PR before), so fork
|
||||||
|
* detection is gated on whether the PR crosses repository boundaries.
|
||||||
|
*
|
||||||
|
* The repository id comparison is the canonical signal; the `head.repo.fork`
|
||||||
|
* flag and a case-insensitive full_name comparison are fallbacks for payloads
|
||||||
|
* where the ids are unavailable (e.g. a deleted head repository).
|
||||||
|
*/
|
||||||
|
private function isForkPullRequest(mixed $payload): bool
|
||||||
|
{
|
||||||
|
$headRepoId = data_get($payload, 'pull_request.head.repo.id');
|
||||||
|
$baseRepoId = data_get($payload, 'pull_request.base.repo.id');
|
||||||
|
|
||||||
|
if ($headRepoId !== null && $baseRepoId !== null) {
|
||||||
|
return (string) $headRepoId !== (string) $baseRepoId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data_get($payload, 'pull_request.head.repo.fork') === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headRepoFullName = data_get($payload, 'pull_request.head.repo.full_name');
|
||||||
|
$baseRepoFullName = data_get($payload, 'pull_request.base.repo.full_name');
|
||||||
|
|
||||||
|
if (is_string($headRepoFullName) && is_string($baseRepoFullName)) {
|
||||||
|
return Str::lower($headRepoFullName) !== Str::lower($baseRepoFullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function redirect(Request $request)
|
public function redirect(Request $request)
|
||||||
{
|
{
|
||||||
$code = (string) $request->query('code', '');
|
$code = (string) $request->query('code', '');
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ public function __construct(
|
||||||
public string $commitSha,
|
public string $commitSha,
|
||||||
public ?string $authorAssociation,
|
public ?string $authorAssociation,
|
||||||
public string $fullName,
|
public string $fullName,
|
||||||
|
public bool $isForkPullRequest = false,
|
||||||
) {
|
) {
|
||||||
$this->onQueue('high');
|
$this->onQueue('high');
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +93,17 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
|
||||||
|
|
||||||
// Check if PR deployments from public contributors are restricted
|
// Check if PR deployments from public contributors are restricted
|
||||||
if (! $application->settings->is_pr_deployments_public_enabled) {
|
if (! $application->settings->is_pr_deployments_public_enabled) {
|
||||||
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
|
// Fork PRs carry untrusted code from a repository outside our control.
|
||||||
|
// GitHub's author_association cannot be trusted to gate these (it grants
|
||||||
|
// CONTRIBUTOR to anyone who has merely opened an issue/PR before), so fork
|
||||||
|
// PRs are never deployed automatically when public previews are off.
|
||||||
|
if ($this->isForkPullRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same-repo (non-fork) branch PRs require push access to the base repo,
|
||||||
|
// so only trusted associations are allowed to trigger a deployment.
|
||||||
|
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR'];
|
||||||
if (! in_array($this->authorAssociation, $trustedAssociations)) {
|
if (! in_array($this->authorAssociation, $trustedAssociations)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
|
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
|
||||||
:canResource="$application" />
|
:canResource="$application" />
|
||||||
<x-forms.checkbox
|
<x-forms.checkbox
|
||||||
helper="When enabled, anyone can trigger PR deployments. When disabled, only repository members, collaborators, and contributors can trigger PR deployments."
|
helper="When enabled, anyone can trigger PR deployments. When disabled, fork PRs are blocked and only repository owners, members, and collaborators can trigger PR deployments."
|
||||||
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
|
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
|
||||||
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
|
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue