diff --git a/.env b/.env index 48243088..863b4776 100644 --- a/.env +++ b/.env @@ -35,7 +35,7 @@ APP_SECRET=c7dbd995176e079f857b6649b60ccf68 # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8" # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml -DATABASE_URL=postgresql://127.0.0.1:5432/main?serverVersion=11&charset=utf8 +DATABASE_URL="postgresql://127.0.0.1:5432/main?serverVersion=17&charset=utf8" ###< doctrine/doctrine-bundle ### ###> symfony/messenger ### @@ -86,11 +86,21 @@ SECURITY_ADVISORIES_DB_DIR=%kernel.project_dir%/var/security-advisories # STORAGE_SOURCE=storage.s3 # STORAGE_AWS_BUCKET=my-bucket-name # STORAGE_AWS_REGION=eu-central-1 -# STORAGE_AWS_OPAQUE_AUTH=false # Either true or false - This will control if you want to use key/secret pair or use machine credentials -# STORAGE_AWS_KEY=example_key # Only necessary if STORAGE_AWS_OPAQUE_AUTH is true -# STORAGE_AWS_SECRET=example_secret # Only necessary if STORAGE_AWS_OPAQUE_AUTH is true -# STORAGE_AWS_ENDPOINT="https://s3.myhost.com" # This is an optional argument, can be used for self-hosted AWS S3 instances -# STORAGE_AWS_PATH_STYLE_ENDPOINT=true # Enables Path Style Endpoint setting +# +# Authentication options: +# Option 1: Explicit credentials (set STORAGE_AWS_OPAQUE_AUTH=true) +# STORAGE_AWS_OPAQUE_AUTH=true +# STORAGE_AWS_KEY=your_access_key_id +# STORAGE_AWS_SECRET=your_secret_access_key +# +# Option 2: AWS credential chain (set STORAGE_AWS_OPAQUE_AUTH=false or omit) +# Supports: environment variables, EKS Pod Identity/IRSA, ECS credentials, EC2 instance profile +# STORAGE_AWS_OPAQUE_AUTH=false +# # No key/secret needed - credentials are obtained automatically from the environment +# +# Optional: Custom S3 endpoint (for S3-compatible storage like MinIO, Ceph, etc.) +# STORAGE_AWS_ENDPOINT="https://s3.myhost.com" +# STORAGE_AWS_PATH_STYLE_ENDPOINT=true # Required for most S3-compatible storage ###< storage ### ###> excelwebzone/recaptcha-bundle ### diff --git a/.env.docker b/.env.docker index 74b445db..e73b2ed5 100644 --- a/.env.docker +++ b/.env.docker @@ -37,9 +37,9 @@ APP_SECRET=c7dbd995176e079f857b6649b60ccf68 ###> doctrine/doctrine-bundle ### # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" -# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8" +# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=17&charset=utf8" # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml -DATABASE_URL=postgresql://main:main@database:5432/main?serverVersion=11&charset=utf8 +DATABASE_URL="postgresql://main:main@database:5432/main?serverVersion=17&charset=utf8" ###< doctrine/doctrine-bundle ### ###> symfony/messenger ### @@ -90,10 +90,21 @@ SECURITY_ADVISORIES_DB_DIR=%kernel.project_dir%/var/security-advisories # STORAGE_SOURCE=storage.s3 # STORAGE_AWS_BUCKET=my-bucket-name # STORAGE_AWS_REGION=eu-central-1 -# STORAGE_AWS_OPAQUE_AUTH=false # Either true or false - This will control if you want to use key/secret pair or use machine credentials -# STORAGE_AWS_KEY=example_key # Only necessary if STORAGE_AWS_OPAQUE_AUTH is true -# STORAGE_AWS_SECRET=example_secret # Only necessary if STORAGE_AWS_OPAQUE_AUTH is true -# STORAGE_AWS_ENDPOINT="https://s3.myhost.com" # This is an optional argument, can be used for self-hosted AWS S3 instances +# +# Authentication options: +# Option 1: Explicit credentials (set STORAGE_AWS_OPAQUE_AUTH=true) +# STORAGE_AWS_OPAQUE_AUTH=true +# STORAGE_AWS_KEY=your_access_key_id +# STORAGE_AWS_SECRET=your_secret_access_key +# +# Option 2: AWS credential chain (set STORAGE_AWS_OPAQUE_AUTH=false or omit) +# Supports: environment variables, EKS Pod Identity/IRSA, ECS credentials, EC2 instance profile +# STORAGE_AWS_OPAQUE_AUTH=false +# # No key/secret needed - credentials are obtained automatically from the environment +# +# Optional: Custom S3 endpoint (for S3-compatible storage like MinIO, Ceph, etc.) +# STORAGE_AWS_ENDPOINT="https://s3.myhost.com" +# STORAGE_AWS_PATH_STYLE_ENDPOINT=true # Required for most S3-compatible storage ###< storage ### ###> excelwebzone/recaptcha-bundle ### diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 22d887ac..193fca51 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -32,14 +32,14 @@ jobs: - "7.4" - "8.0" postgresql: - - "11" + - "17" include: - php: "7.4" php-version: "7.4.2" - php: "8.0" php-version: "8.0.30" - - postgresql: "11" - postgresql-version: "11.7" + - postgresql: "17" + postgresql-version: "17" steps: - name: "Install OS dependencies" diff --git a/README.md b/README.md index 5f6a5286..aef4e247 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Documentation: [https://repman.io/docs/](https://repman.io/docs/) ## Requirements - PHP >= 7.4 -- PostgreSQL 11 +- PostgreSQL 11 - 17 - `var` dir must be writeable - any web server @@ -47,6 +47,7 @@ composer install ``` Setup database: + ``` bin/console doctrine:migrations:migrate #for postgres bin/console doctrine:schema:create #for sqlite init as migrations are only postgres-compatible @@ -62,9 +63,11 @@ To configure mailer transport, enter connection details in the `MAILER_DSN` envi ``` MAILER_DSN=smtp://user:pass@smtp.example.com ``` + Read more: [transport setup](https://symfony.com/doc/current/mailer.html#transport-setup) In addition, setup also `MAILER_SENDER` environment variable + ``` MAILER_SENDER=mail_from@example.com ``` @@ -140,10 +143,71 @@ Scopes: ## Self-hosted GitLab To integrate with self-hosted GitLab, enter the instance url in the `APP_GITLAB_API_URL` environment variable + ``` APP_GITLAB_API_URL='https://gitlab.organization.lan' ``` +## Self-hosted Storage + +By default, Repman stores packages locally on disk. For production deployments, you can use S3-compatible storage. + +### Local Storage (Default) + +```env +STORAGE_SOURCE=storage.local +PROXY_DIST_DIR=%kernel.project_dir%/var/proxy +PACKAGES_DIST_DIR=%kernel.project_dir%/var/repo +``` + +### S3 Storage + +To use AWS S3 or S3-compatible storage (MinIO, Ceph, etc.): + +```env +STORAGE_SOURCE=storage.s3 +STORAGE_AWS_BUCKET=my-bucket-name +STORAGE_AWS_REGION=eu-west-1 +# When using S3, use relative paths: +PROXY_DIST_DIR=var/proxy +PACKAGES_DIST_DIR=var/repo +``` + +#### Authentication Options + +**Option 1: AWS Credential Chain (Recommended for AWS/EKS)** + +Uses the AWS SDK's default credential provider chain, which supports: + +- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) +- EKS Pod Identity / IRSA (IAM Roles for Service Accounts) +- ECS container credentials +- EC2 instance profile credentials + +```env +STORAGE_AWS_OPAQUE_AUTH=false +# No key/secret needed - credentials obtained automatically +``` + +**Option 2: Explicit Credentials** + +For environments where you need to provide credentials directly: + +```env +STORAGE_AWS_OPAQUE_AUTH=true +STORAGE_AWS_KEY=your_access_key_id +STORAGE_AWS_SECRET=your_secret_access_key +``` + +#### S3-Compatible Storage (MinIO, Ceph, etc.) + +For self-hosted S3-compatible storage: + +```env +STORAGE_AWS_ENDPOINT=https://s3.myhost.com +STORAGE_AWS_PATH_STYLE_ENDPOINT=true +``` + ## Docker - Override with `docker-compose.override.yml` if needed. @@ -166,9 +230,9 @@ docker-compose up In case of any problems, you can use: - - Our documentation: [repman.io/docs](https://repman.io/docs/) - it is also open sourced [github.com/repman-io/repman-docs-pages](https://github.com/repman-io/repman-docs-pages) - - GitHub issue list: [github.com/repman-io/repman/issues](https://github.com/repman-io/repman/issues) - feel free to create a new issue there - - E-mail: contact [at] repman.io +- Our documentation: [repman.io/docs](https://repman.io/docs/) - it is also open sourced [github.com/repman-io/repman-docs-pages](https://github.com/repman-io/repman-docs-pages) +- GitHub issue list: [github.com/repman-io/repman/issues](https://github.com/repman-io/repman/issues) - feel free to create a new issue there +- E-mail: contact [at] repman.io ## License @@ -178,11 +242,11 @@ However, Repman includes several third-party Open-Source libraries, which are li #### Libraries or projects directly included in Repman - - Tabler: [MIT](https://github.com/tabler/tabler/blob/master/LICENSE) - - Feather: [MIT](https://github.com/feathericons/feather/blob/master/LICENSE) - - Lucide: License: [ISC](https://github.com/lucide-icons/lucide/blob/master/LICENSE) - - Postmark Transactional Email Templates: [MIT](https://github.com/wildbit/postmark-templates/blob/master/LICENSE) - - Libraries dynamically referenced via Composer: run `composer license` to get the latest licensing info about all dependencies. +- Tabler: [MIT](https://github.com/tabler/tabler/blob/master/LICENSE) +- Feather: [MIT](https://github.com/feathericons/feather/blob/master/LICENSE) +- Lucide: License: [ISC](https://github.com/lucide-icons/lucide/blob/master/LICENSE) +- Postmark Transactional Email Templates: [MIT](https://github.com/wildbit/postmark-templates/blob/master/LICENSE) +- Libraries dynamically referenced via Composer: run `composer license` to get the latest licensing info about all dependencies. --- diff --git a/ansible/vars/public.yml b/ansible/vars/public.yml index 8440ce79..50c0e59f 100644 --- a/ansible/vars/public.yml +++ b/ansible/vars/public.yml @@ -38,7 +38,7 @@ db_user: repman db_password: "{{ lookup('env', 'REPMAN_DB_PASSWORD') or 'password' }}" # Postgres -postgresql_version: 11 +postgresql_version: 17 postgresql_port: "{{ db_port }}" postgresql_encoding: 'UTF-8' postgresql_locale: 'en_US.UTF-8' diff --git a/buddy.yml b/buddy.yml index 2ff9f1e7..0fbd2c5e 100644 --- a/buddy.yml +++ b/buddy.yml @@ -44,7 +44,7 @@ "git config --global user.name \"buddy.works\"" services: - type: "POSTGRE_SQL" - version: "11.6-alpine" + version: "17-alpine" connection: host: "postgres" port: 5432 @@ -66,7 +66,7 @@ next_pipeline_name: "deploy" variables: - key: "DATABASE_URL" - value: "postgresql://main:main@postgres:5432/main?serverVersion=11&charset=utf8" + value: "postgresql://main:main@postgres:5432/main?serverVersion=17&charset=utf8" id: 352921 description: "" - pipeline: "security" @@ -102,7 +102,7 @@ integration_hash: "5f4f3794fd5a7366872df494" variables: - key: "DATABASE_URL" - value: "postgresql://main:main@postgres:5432/main?serverVersion=11&charset=utf8" + value: "postgresql://main:main@postgres:5432/main?serverVersion=17&charset=utf8" - pipeline: "setup" trigger_mode: "MANUAL" ref_name: "refs/heads/*" diff --git a/docker-compose.yml b/docker-compose.yml index 4e9dda6e..0bd6e64b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,10 @@ -version: '3.4' - x-restart-policy: &restart_policy restart: unless-stopped services: database: << : *restart_policy - image: postgres:11.20-alpine + image: postgres:17-alpine logging: driver: none environment: diff --git a/src/Service/Integration/Aws/S3AdapterFactory.php b/src/Service/Integration/Aws/S3AdapterFactory.php index 6c5470f9..d992b327 100644 --- a/src/Service/Integration/Aws/S3AdapterFactory.php +++ b/src/Service/Integration/Aws/S3AdapterFactory.php @@ -4,8 +4,21 @@ namespace Buddy\Repman\Service\Integration\Aws; +use Aws\Credentials\CredentialProvider; use Aws\S3\S3Client; +/** + * Factory for creating S3Client instances with flexible authentication. + * + * Supports multiple authentication methods: + * - Explicit credentials (STORAGE_AWS_OPAQUE_AUTH=true): Uses provided key/secret + * - Default credential chain (STORAGE_AWS_OPAQUE_AUTH=false): Uses AWS SDK's default + * credential provider chain which supports: + * - Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) + * - Web identity token (EKS Pod Identity / IRSA) + * - ECS container credentials + * - EC2 instance profile credentials + */ final class S3AdapterFactory { private string $region; @@ -55,10 +68,16 @@ public function create(): S3Client ]; if ($this->isOpaqueAuth) { + // Use explicit credentials when provided $args['credentials'] = [ 'key' => $this->key, 'secret' => $this->secret, ]; + } else { + // Use the default credential provider chain with memoization + // This supports: env vars, web identity (Pod Identity/IRSA), ECS, instance profile + $provider = CredentialProvider::defaultProvider(); + $args['credentials'] = CredentialProvider::memoize($provider); } if ($this->endpoint !== null) { diff --git a/tests/Unit/Service/Integration/Aws/S3AdapterFactoryTest.php b/tests/Unit/Service/Integration/Aws/S3AdapterFactoryTest.php index ce3c23e0..dd5d56e8 100644 --- a/tests/Unit/Service/Integration/Aws/S3AdapterFactoryTest.php +++ b/tests/Unit/Service/Integration/Aws/S3AdapterFactoryTest.php @@ -10,7 +10,7 @@ class S3AdapterFactoryTest extends TestCase { - public function testCreateWithMachineCredentials(): void + public function testCreateWithDefaultCredentialChain(): void { $factory = new S3AdapterFactory('eu-east-1', false, '', ''); @@ -19,6 +19,94 @@ public function testCreateWithMachineCredentials(): void self::assertSame('eu-east-1', $cfg['signing_region']); self::assertSame('s3v4', $cfg['signature_version']); + // The 'credentials' key should NOT be in config when using provider chain + self::assertArrayNotHasKey('credentials', $cfg); + } + + public function testCreateWithDefaultCredentialChainUsesProvider(): void + { + $factory = new S3AdapterFactory('eu-east-1', false); + + $instance = $factory->create(); + $cfg = $instance->getConfig(); + + // When using the default provider chain, credentials are not in config + self::assertArrayNotHasKey('credentials', $cfg); + } + + /** + * Verifies that when isOpaqueAuth=false, the credential chain is used + * and static credentials are NOT embedded in the client config. + */ + public function testCredentialChainIsUsedWhenOpaqueAuthIsFalse(): void + { + // Even if key/secret are provided, they should be ignored when isOpaqueAuth=false + $factory = new S3AdapterFactory('eu-west-1', false, 'ignored-key', 'ignored-secret'); + + $instance = $factory->create(); + $cfg = $instance->getConfig(); + + // The 'credentials' key should NOT contain static credentials array + // Instead, the SDK uses the credential provider chain internally + self::assertArrayNotHasKey('credentials', $cfg); + + // Verify the client is configured correctly + self::assertSame('eu-west-1', $cfg['signing_region']); + } + + /** + * Verifies that when isOpaqueAuth=true, static credentials are used + * and the credential chain is NOT used. + */ + public function testStaticCredentialsAreUsedWhenOpaqueAuthIsTrue(): void + { + $factory = new S3AdapterFactory('eu-west-1', true, 'my-access-key', 'my-secret-key'); + + $instance = $factory->create(); + + // When using opaque auth, credentials should resolve to the provided values + $creds = $instance->getCredentials()->wait(); + + self::assertSame('my-access-key', $creds->getAccessKeyId()); + self::assertSame('my-secret-key', $creds->getSecretKey()); + } + + /** + * Verifies credential chain works with environment variables when available. + * This test sets temporary env vars to simulate AWS credential environment. + */ + public function testCredentialChainResolvesFromEnvironmentVariables(): void + { + // Store original values + $originalKey = getenv('AWS_ACCESS_KEY_ID'); + $originalSecret = getenv('AWS_SECRET_ACCESS_KEY'); + + try { + // Set test credentials in environment + putenv('AWS_ACCESS_KEY_ID=env-test-key'); + putenv('AWS_SECRET_ACCESS_KEY=env-test-secret'); + + $factory = new S3AdapterFactory('eu-west-1', false); + $instance = $factory->create(); + + // The credential chain should resolve from environment variables + $creds = $instance->getCredentials()->wait(); + + self::assertSame('env-test-key', $creds->getAccessKeyId()); + self::assertSame('env-test-secret', $creds->getSecretKey()); + } finally { + // Restore original environment + if ($originalKey === false) { + putenv('AWS_ACCESS_KEY_ID'); + } else { + putenv("AWS_ACCESS_KEY_ID=$originalKey"); + } + if ($originalSecret === false) { + putenv('AWS_SECRET_ACCESS_KEY'); + } else { + putenv("AWS_SECRET_ACCESS_KEY=$originalSecret"); + } + } } public function testCreateWithOpaqueCredentials(): void