From c5677539e4f3f973d10f755639e19fb995047021 Mon Sep 17 00:00:00 2001 From: biondizzle <32694450+biondizzle@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:07:14 -0400 Subject: [PATCH] Add presigned URL management --- migrations/Version20250605112000.php | 26 ++++ src/Controller/ConsoleApiController.php | 5 +- src/Entity/S3PresignedUrl.php | 5 + src/Service/S3Service.php | 53 ++++++--- templates/console/index.html.twig | 150 ++++++++++++++++++++++++ templates/openapi/openapi.yaml | 3 + templates/openapi/schemas.yaml | 3 + 7 files changed, 228 insertions(+), 17 deletions(-) create mode 100644 migrations/Version20250605112000.php diff --git a/migrations/Version20250605112000.php b/migrations/Version20250605112000.php new file mode 100644 index 0000000..906ba98 --- /dev/null +++ b/migrations/Version20250605112000.php @@ -0,0 +1,26 @@ +addSql("ALTER TABLE s3_presigned_urls ADD url VARCHAR(2048) NOT NULL"); + } + + public function down(Schema $schema): void + { + $this->addSql("ALTER TABLE s3_presigned_urls DROP COLUMN url"); + } +} diff --git a/src/Controller/ConsoleApiController.php b/src/Controller/ConsoleApiController.php index c4ae4ee..8890554 100644 --- a/src/Controller/ConsoleApiController.php +++ b/src/Controller/ConsoleApiController.php @@ -389,7 +389,8 @@ class ConsoleApiController extends AbstractController 'method' => $url->getMethod(), 'access_key' => $url->getAccessKey(), 'expires_at' => $url->getExpiresAt()->format('Y-m-d H:i:s'), - 'created_at' => $url->getCreatedAt()->format('Y-m-d H:i:s') + 'created_at' => $url->getCreatedAt()->format('Y-m-d H:i:s'), + 'url' => $url->getUrl() ]; }, $urls) ]); @@ -413,7 +414,7 @@ class ConsoleApiController extends AbstractController return new JsonResponse(['error' => 'Invalid access key'], 404); } - $url = $this->s3Service->generatePresignedGetUrl($bucketName, $objectKey, $credential, $expiresIn); + $url = $this->s3Service->generatePresignedUrl($bucketName, $objectKey, $credential, $method, $expiresIn); return new JsonResponse(['url' => $url], 201); } diff --git a/src/Entity/S3PresignedUrl.php b/src/Entity/S3PresignedUrl.php index 62dbe6c..07167ff 100644 --- a/src/Entity/S3PresignedUrl.php +++ b/src/Entity/S3PresignedUrl.php @@ -40,6 +40,9 @@ class S3PresignedUrl #[ORM\Column(type: 'datetime')] private \DateTime $createdAt; + #[ORM\Column(type: 'string', length: 2048)] + private string $url; + public function __construct() { $this->createdAt = new \DateTime(); @@ -59,6 +62,8 @@ class S3PresignedUrl public function getAccessKey(): string { return $this->accessKey; } public function setAccessKey(string $accessKey): self { $this->accessKey = $accessKey; return $this; } public function getCreatedAt(): \DateTime { return $this->createdAt; } + public function getUrl(): string { return $this->url; } + public function setUrl(string $url): self { $this->url = $url; return $this; } public function isExpired(): bool { diff --git a/src/Service/S3Service.php b/src/Service/S3Service.php index 5946ab0..2c55dc8 100644 --- a/src/Service/S3Service.php +++ b/src/Service/S3Service.php @@ -403,31 +403,54 @@ class S3Service } // AWS Signature V4 helpers - public function generatePresignedGetUrl(string $bucketName, string $objectKey, S3Credential $credential, int $expiresIn = 3600): string + public function generatePresignedUrl(string $bucketName, string $objectKey, S3Credential $credential, string $method = 'GET', int $expiresIn = 3600): string { $expiresAt = new \DateTime('+' . $expiresIn . ' seconds'); - - // Store in database for tracking - $presignedUrl = $this->createPresignedUrl($bucketName, $objectKey, 'GET', $expiresAt, $credential->getAccessKey()); - - // Generate the actual presigned URL (simplified - you'll need full AWS Signature V4 implementation) + + // Store entry in DB + $presignedUrl = $this->createPresignedUrl($bucketName, $objectKey, $method, $expiresAt, $credential->getAccessKey()); + + $amzDate = $presignedUrl->getCreatedAt()->format('Ymd\THis\Z'); + $shortDate = $presignedUrl->getCreatedAt()->format('Ymd'); + $scope = $shortDate . '/us-east-1/s3/aws4_request'; + $params = [ 'X-Amz-Algorithm' => 'AWS4-HMAC-SHA256', - 'X-Amz-Credential' => $credential->getAccessKey() . '/' . date('Ymd') . '/us-east-1/s3/aws4_request', - 'X-Amz-Date' => gmdate('Ymd\THis\Z'), + 'X-Amz-Credential' => $credential->getAccessKey() . '/' . $scope, + 'X-Amz-Date' => $amzDate, 'X-Amz-Expires' => $expiresIn, - 'X-Amz-SignedHeaders' => 'host', - 'X-Amz-Signature' => $this->calculateSignature($bucketName, $objectKey, $params, $credential->getSecretKey()), - 'hash' => $presignedUrl->getUrlHash() + 'X-Amz-SignedHeaders' => 'host' ]; - return '/' . $bucketName . '/' . $objectKey . '?' . http_build_query($params); + $canonicalQuery = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + $canonicalRequest = sprintf("%s\n/%s/%s\n%s\nhost:%s\n\nhost\nUNSIGNED-PAYLOAD", $method, $bucketName, $objectKey, $canonicalQuery, 'localhost'); + + $stringToSign = "AWS4-HMAC-SHA256\n" . $amzDate . "\n" . $scope . "\n" . hash('sha256', $canonicalRequest); + $signingKey = $this->deriveSigningKey($credential->getSecretKey(), $shortDate, 'us-east-1', 's3'); + $signature = hash_hmac('sha256', $stringToSign, $signingKey); + + $params['X-Amz-Signature'] = $signature; + $params['hash'] = $presignedUrl->getUrlHash(); + + $url = '/' . $bucketName . '/' . $objectKey . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986); + $presignedUrl->setUrl($url); + $this->entityManager->flush(); + + return $url; } - private function calculateSignature(string $bucketName, string $objectKey, array $params, string $secretKey): string + // Deprecated - kept for backward compatibility + public function generatePresignedGetUrl(string $bucketName, string $objectKey, S3Credential $credential, int $expiresIn = 3600): string { - // Simplified signature calculation - implement full AWS Signature V4 here - return hash_hmac('sha256', $bucketName . $objectKey . serialize($params), $secretKey); + return $this->generatePresignedUrl($bucketName, $objectKey, $credential, 'GET', $expiresIn); + } + + private function deriveSigningKey(string $secretKey, string $date, string $region, string $service) + { + $kDate = hash_hmac('sha256', $date, 'AWS4' . $secretKey, true); + $kRegion = hash_hmac('sha256', $region, $kDate, true); + $kService = hash_hmac('sha256', $service, $kRegion, true); + return hash_hmac('sha256', 'aws4_request', $kService, true); } /** diff --git a/templates/console/index.html.twig b/templates/console/index.html.twig index b3760d2..f510f49 100644 --- a/templates/console/index.html.twig +++ b/templates/console/index.html.twig @@ -577,6 +577,59 @@ + +
+
+

Presigned URLs

+ +
+ +
+ +
+
+
Loading URLs...
+ + + + +
+
+
+ + + + @@ -664,10 +769,12 @@ selectedBucket: null, filteredObjects: [], objectSearch: '', + presignedUrls: [], // Modals showCreateCredentialModal: false, showCreateBucketModal: false, + showCreatePresignedModal: false, // Forms newCredential: { @@ -680,6 +787,13 @@ owner_id: '', region: 'us-east-1' }, + newPresigned: { + bucket_name: '', + object_key: '', + method: 'GET', + expires_in: 3600, + access_key: '' + }, init() { this.loadStats(); @@ -699,6 +813,9 @@ case 'buckets': this.loadBuckets(); break; + case 'presigned': + this.loadPresignedUrls(); + break; } }, @@ -795,6 +912,39 @@ } }, + async loadPresignedUrls() { + this.loading = true; + try { + const response = await this.apiCall('/presigned-urls'); + this.presignedUrls = response.urls; + } catch (error) { + console.error('Failed to load presigned URLs:', error); + } + this.loading = false; + }, + + async createPresigned() { + try { + const payload = { ...this.newPresigned }; + const urlData = await this.apiCall('/presigned-urls', { + method: 'POST', + body: JSON.stringify(payload) + }); + this.showMessage('Presigned URL created'); + this.showCreatePresignedModal = false; + this.newPresigned = { bucket_name: '', object_key: '', method: 'GET', expires_in: 3600, access_key: '' }; + this.presignedUrls.unshift({ + bucket_name: payload.bucket_name, + object_key: payload.object_key, + method: payload.method, + expires_at: new Date(Date.now() + payload.expires_in * 1000).toISOString().slice(0,19).replace('T',' '), + url: urlData.url + }); + } catch (error) { + console.error('Failed to create presigned URL:', error); + } + }, + async deleteCredential(id) { if (!confirm('Are you sure you want to delete this credential?')) return; diff --git a/templates/openapi/openapi.yaml b/templates/openapi/openapi.yaml index c6a1af1..502c284 100644 --- a/templates/openapi/openapi.yaml +++ b/templates/openapi/openapi.yaml @@ -1091,6 +1091,9 @@ components: type: string format: date-time example: '2023-01-15T10:30:00Z' + url: + type: string + example: "/my-bucket/file.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&..." PresignedUrlsList: type: object properties: diff --git a/templates/openapi/schemas.yaml b/templates/openapi/schemas.yaml index 4c7f901..ea49e1e 100644 --- a/templates/openapi/schemas.yaml +++ b/templates/openapi/schemas.yaml @@ -587,6 +587,9 @@ PresignedUrlInfo: type: string format: date-time example: "2023-01-15T10:30:00Z" + url: + type: string + example: "/my-bucket/file.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&..." PresignedUrlsList: type: object