Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions server/src/Http/Controllers/Internal/v1/ProofController.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ public function captureSignature(string $publicId, Request $request)
// set the signature storage path
$path = $this->signatureStoragePath($proof);

// upload signature
$this->storeSignature($path, base64_decode($signature), 'public');
// upload signature (private object; File::url serves it through a signed URL)
$this->storeSignature($path, base64_decode($signature));

// create file record for upload
$file = $this->createSignatureFile($path, $signature, $proof);
Expand Down Expand Up @@ -121,9 +121,13 @@ protected function createProof(array $attributes): Proof
return Proof::create($attributes);
}

protected function storeSignature(string $path, string|false $contents, string $visibility): void
/**
* Writes without an ACL: the media bucket enforces bucket-owner object ownership, so a
* 'public' visibility (public-read ACL) makes S3 reject the PUT and put() return false.
*/
protected function storeSignature(string $path, string|false $contents): void
{
Storage::disk('s3')->put($path, $contents, $visibility);
Storage::disk('s3')->put($path, $contents);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion server/src/Models/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ public function getAvatarUrlAttribute($value): ?string
return static::getAvatar($value);
}

return $value;
// legacy rows hold an absolute URL; re-sign it if it points into the private media bucket
return Utils::signStoredFileUrl($value);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion server/src/Models/Place.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ public function getAvatarUrlAttribute($value)
return static::getAvatar($value);
}

return $value;
// legacy rows hold an absolute URL; re-sign it if it points into the private media bucket
return Utils::signStoredFileUrl($value);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion server/src/Models/Vehicle.php
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,8 @@ public function getAvatarUrlAttribute($value)
return static::getAvatar($value);
}

return $value;
// legacy rows hold an absolute URL; re-sign it if it points into the private media bucket
return Utils::signStoredFileUrl($value);
}

/**
Expand Down
11 changes: 11 additions & 0 deletions server/src/Support/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -1603,4 +1603,15 @@ public static function fixPhone(?string $phone): ?string

return $phone;
}

/**
* Re-sign a URL stored as a string (e.g. a legacy `avatar_url` value) when it points into the
* private S3 media bucket, so it keeps working once the bucket stops allowing public reads.
* Other values pass through unchanged. Falls back to the raw value on core-api releases that
* predate File::signStoredUrl().
*/
public static function signStoredFileUrl(?string $url): ?string
{
return method_exists(\Fleetbase\Models\File::class, 'signStoredUrl') ? \Fleetbase\Models\File::signStoredUrl($url) : $url;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ protected function createProof(array $attributes): Proof
return $proof;
}

protected function storeSignature(string $path, string|false $contents, string $visibility): void
protected function storeSignature(string $path, string|false $contents): void
{
$this->storedSignatures[] = [$path, $contents, $visibility];
$this->storedSignatures[] = [$path, $contents];
}

protected function createSignatureFile(string $path, string $signature, Proof $proof): Fleetbase\Models\File
Expand Down Expand Up @@ -206,7 +206,7 @@ public function setKey($model, $type = null): Fleetbase\Models\File
'raw_data' => $signature,
])
->and($controller->storedSignatures)->toBe([
['uploads/company-uuid/signatures/proof-public-1.png', 'signature-bytes', 'public'],
['uploads/company-uuid/signatures/proof-public-1.png', 'signature-bytes'],
])
->and($controller->createdFiles)->toBe([
['uploads/company-uuid/signatures/proof-public-1.png', $signature, 'proof-public-1'],
Expand Down
7 changes: 4 additions & 3 deletions server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public function disk($disk = null)

public function put($path, $contents, $options = [])
{
$this->writes[] = [$path];
$this->writes[] = [$path, $options];

return true;
}
Expand Down Expand Up @@ -200,8 +200,9 @@ public function put($path, $contents, $options = [])
expect($proof)->toBeInstanceOf(Proof::class)
->and($connection->table('proofs')->count())->toBe(1);

$probe->callHelper('storeSignature', 'signatures/proof.png', 'binary', 'public');
expect($GLOBALS['fleetopsProofStorageFake']->writes)->toHaveCount(1);
$probe->callHelper('storeSignature', 'signatures/proof.png', 'binary');
// Signatures are written without a visibility/ACL option; the media bucket rejects ACLs.
expect($GLOBALS['fleetopsProofStorageFake']->writes)->toBe([['signatures/proof.png', []]]);

expect($probe->callHelper('jsonResponse', ['status' => 'ok'])->getData(true))->toBe(['status' => 'ok'])
->and($probe->callHelper('errorResponse', 'nope')->getData(true)['error'])->toBe('nope')
Expand Down
9 changes: 9 additions & 0 deletions server/tests/Unit/Support/UtilsAdditionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ function fleetopsUtilsAdditionalPolygon(): Polygon
]);
}

test('stored file url signing passes through values that are not absolute urls', function () {
// Absolute URLs into the media bucket are re-signed by core-api File::signStoredUrl (covered there);
// everything else, including avatar option keys and empty values, must come back untouched.
expect(Utils::signStoredFileUrl(null))->toBeNull()
->and(Utils::signStoredFileUrl(''))->toBe('')
->and(Utils::signStoredFileUrl('mini_bus'))->toBe('mini_bus')
->and(Utils::signStoredFileUrl('custom-avatars/vehicles/c/van.png'))->toBe('custom-avatars/vehicles/c/van.png');
});

test('company transaction currency prefers organization currency and falls back to usd', function () {
$company = new Company();
$company->setRawAttributes([
Expand Down
Loading