diff --git a/.gitignore b/.gitignore index 76655919c..e57afb074 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ yarn-error.log ###> phpstan/phpstan ### phpstan.neon ###< phpstan/phpstan ### + +# Claude Code project documentation (local development only) +CLAUDE.md diff --git a/config/parameters.yaml b/config/parameters.yaml index 154fbd8a5..6ecd4e9cc 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -43,7 +43,6 @@ parameters: ###################################################################################################################### partdb.saml.enabled: '%env(bool:SAML_ENABLED)%' # If this is set to true, SAML authentication is enabled - ###################################################################################################################### # Miscellaneous ###################################################################################################################### @@ -104,3 +103,5 @@ parameters: env(SAML_ROLE_MAPPING): '{}' env(DATABASE_EMULATE_NATURAL_SORT): 0 + + env(INITIAL_ADMIN_API_KEY): '' diff --git a/docs/api/authentication.md b/docs/api/authentication.md index b386c0cdb..f30e4276e 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -32,6 +32,14 @@ tokens as you want and also delete them again. When deleting a token, it is immediately invalidated and can not be used anymore, which means that the application can not access the API anymore with this token. +### Initial Admin API Token + +For automated deployments and CI/CD pipelines, Part-DB supports automatically creating an initial admin API token +during database setup. Set the `INITIAL_ADMIN_API_KEY` environment variable to a 64-character random string +(generate with `openssl rand -hex 32`) before running database migrations. Part-DB will create an API token named +"Initial Admin Token" with FULL scope that expires after 1 year. The token can be used immediately with the format +`Bearer tcp_` in the Authorization header. + ### Token permissions and scopes API tokens are ultimately limited by the permissions of the user, which belongs to the token. That means that the token diff --git a/docs/configuration.md b/docs/configuration.md index d4b217816..04653ec3b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -114,6 +114,11 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept particularly for securing and protecting various aspects of your application. It's a secret key that is used for cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this value should be handled as confidential data and not shared publicly. +* `INITIAL_ADMIN_API_KEY` (env only): When set to a 64-character random string (generate with `openssl rand -hex 32`), + Part-DB will automatically create an API token named "Initial Admin Token" for the admin user during database + migrations. This token will have FULL scope and expire after 1 year. This is useful for automated deployments, + CI/CD pipelines, and Docker setups where you need immediate API access without manual token creation. The token + can be used with the format `Bearer tcp_` in the Authorization header. * `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the part image gallery diff --git a/migrations/Version20250907000000.php b/migrations/Version20250907000000.php new file mode 100644 index 000000000..1a2a33bdc --- /dev/null +++ b/migrations/Version20250907000000.php @@ -0,0 +1,71 @@ +getInitialAdminApiToken(); + if (empty($apiToken)) { + return; + } + + // Create a proper API token with the 'tcp_' prefix and the provided key + $fullToken = 'tcp_' . $apiToken; + + // Set expiration to 1 year from now + $validUntil = date('Y-m-d H:i:s', strtotime('+1 year')); + $currentDateTime = date('Y-m-d H:i:s'); + + // Insert the API token for the admin user (user_id = 2) + // Level 4 = FULL access (can do everything the user can do) + $sql = "INSERT INTO api_tokens (user_id, name, token, level, valid_until, datetime_added, last_modified) + VALUES (2, 'Initial Admin Token', ?, 4, ?, ?, ?)"; + + $this->addSql($sql, [$fullToken, $validUntil, $currentDateTime, $currentDateTime]); + } + + public function mySQLUp(Schema $schema): void + { + $this->createInitialAdminApiToken(); + } + + public function mySQLDown(Schema $schema): void + { + // Remove the initial admin token if it exists + $this->addSql("DELETE FROM api_tokens WHERE name = 'Initial Admin Token' AND user_id = 2"); + } + + public function sqLiteUp(Schema $schema): void + { + $this->createInitialAdminApiToken(); + } + + public function sqLiteDown(Schema $schema): void + { + // Remove the initial admin token if it exists + $this->addSql("DELETE FROM api_tokens WHERE name = 'Initial Admin Token' AND user_id = 2"); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->createInitialAdminApiToken(); + } + + public function postgreSQLDown(Schema $schema): void + { + // Remove the initial admin token if it exists + $this->addSql("DELETE FROM api_tokens WHERE name = 'Initial Admin Token' AND user_id = 2"); + } +} \ No newline at end of file diff --git a/src/Migration/AbstractMultiPlatformMigration.php b/src/Migration/AbstractMultiPlatformMigration.php index bc2b3f191..5cd2c3381 100644 --- a/src/Migration/AbstractMultiPlatformMigration.php +++ b/src/Migration/AbstractMultiPlatformMigration.php @@ -34,7 +34,8 @@ abstract class AbstractMultiPlatformMigration extends AbstractMigration { final public const ADMIN_PW_LENGTH = 10; - protected string $admin_pw = ''; + protected ?string $admin_pw = null; + protected ?string $admin_api_token = null; /** @noinspection SenselessProxyMethodInspection * This method is required to redefine the logger type hint to protected @@ -96,7 +97,7 @@ public function getOldDBVersion(): int */ public function getInitalAdminPW(): string { - if ($this->admin_pw === '') { + if ($this->admin_pw === null) { if (!empty($_ENV['INITIAL_ADMIN_PW'])) { $this->admin_pw = $_ENV['INITIAL_ADMIN_PW']; } else { @@ -108,6 +109,28 @@ public function getInitalAdminPW(): string return password_hash((string) $this->admin_pw, PASSWORD_DEFAULT); } + /** + * Returns the initial admin API token if configured via environment variable. + * If not configured, returns empty string (no token will be created). + */ + public function getInitialAdminApiToken(): string + { + if ($this->admin_api_token === null) { + $apiKey = $_ENV('INITIAL_ADMIN_API_KEY'); + if (!empty($apiKey)) { + //Ensure the length of the API key is correct + if (strlen($apiKey) < 64) { + $this->abortIf(true, 'The provided INITIAL_ADMIN_API_KEY is too short! It must be at least 64 characters long! You can generate a valid key with "openssl rand -hex 32"'); + } + + // Use the provided API key directly (should be generated with openssl rand -hex 32) + $this->admin_api_token = $apiKey; + } + } + + return $this->admin_api_token; + } + public function postUp(Schema $schema): void { parent::postUp($schema); @@ -117,6 +140,13 @@ public function postUp(Schema $schema): void $this->logger->warning('The initial password for the "admin" user is: '.$this->admin_pw.''); $this->logger->warning(''); } + + if ($this->admin_api_token !== '') { + $this->logger->warning(''); + $this->logger->warning('Initial admin API token has been created with the provided key'); + $this->logger->warning('Use this token in Authorization header: Bearer tcp_'.$this->admin_api_token.''); + $this->logger->warning(''); + } } /**