From 554131d6591b7dede8fe6174f44924c98f8c8f96 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 2 Jul 2026 15:45:02 +0200 Subject: [PATCH 1/7] Added API key on users --- CHANGELOG.md | 2 ++ README.md | 18 ++++++++++ config/packages/security.yaml | 9 +++-- migrations/Version20260702123347.php | 33 ++++++++++++++++++ src/Command/UserSetApiKeyCommand.php | 52 ++++++++++++++++++++++++++++ src/Entity/Server.php | 26 +++----------- src/Entity/User.php | 4 +++ src/Security/ApiKeyAuthenticator.php | 18 +++++++++- src/Trait/ApiKeyEntityTrait.php | 35 +++++++++++++++++++ 9 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 migrations/Version20260702123347.php create mode 100644 src/Command/UserSetApiKeyCommand.php create mode 100644 src/Trait/ApiKeyEntityTrait.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1daa21..924edfbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [#88](https://github.com/itk-dev/devops_itksites/pull/88) + - Let users use the API - [#80](https://github.com/itk-dev/devops_itksites/pull/80) 5566: Service agreements - Add security contract entity with crud controller - Add Abstract full crud controller and extend on it in some cases diff --git a/README.md b/README.md index fc1305f9..797838ed 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,24 @@ The system is build so that all analyzed data can be truncated safely and rebuil by "replaying" the DetectionResults. This means that care must be taken when manually maintained data and auto updated data must have cross references. +## API + +Authenticated users can access a simple read-only API – see the API documentation on `/api/docs` for details. + +### API keys + +Run the `app:user:set-api-key` console command to set the API for a user: + +``` shell +docker compose exec phpfpm php bin/console app:user:set-api-key +``` + +Use the API key to make an authenticated request, e.g. + +``` shell +curl --header 'accept: application/json' --header 'authorization: Apikey ' https://itksites.local.itkdev.dk/api/sites +``` + ## Development ```sh diff --git a/config/packages/security.yaml b/config/packages/security.yaml index f57e0e79..b7b82574 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -12,6 +12,11 @@ security: entity: class: App\Entity\User property: email + + app_api_users: + chain: + providers: [app_server_provider, app_user_provider] + firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -20,7 +25,7 @@ security: pattern: ^/api custom_authenticators: - App\Security\ApiKeyAuthenticator - provider: app_server_provider + provider: app_api_users main: custom_authenticators: @@ -41,7 +46,7 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/api/docs, roles: PUBLIC_ACCESS } - - { path: ^/api, roles: ROLE_SERVER } + - { path: ^/api, roles: [ROLE_SERVER, ROLE_USER] } - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } diff --git a/migrations/Version20260702123347.php b/migrations/Version20260702123347.php new file mode 100644 index 00000000..5552bbd6 --- /dev/null +++ b/migrations/Version20260702123347.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE user ADD api_key VARCHAR(255) NOT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649C912ED9D ON user (api_key)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX UNIQ_8D93D649C912ED9D ON user'); + $this->addSql('ALTER TABLE user DROP api_key'); + } +} diff --git a/src/Command/UserSetApiKeyCommand.php b/src/Command/UserSetApiKeyCommand.php new file mode 100644 index 00000000..8a0bd311 --- /dev/null +++ b/src/Command/UserSetApiKeyCommand.php @@ -0,0 +1,52 @@ +userRepository->findOneBy(['email' => $userId]) + ?? $this->userRepository->findOneBy(['name' => $userId]); + + if (null === $user) { + throw new InvalidArgumentException(sprintf('Cannot load user with id %s', $userId)); + } + + $question = sprintf('Really set API key on user %s', $user->getUserIdentifier()); + if (!$io->confirm($question)) { + return Command::SUCCESS; + } + + $user->setApiKey($user->generateApiKey()); + $this->entityManager->flush(); + + $io->success([ + sprintf('API key for user %s set to', $user->getUserIdentifier()), + $user->getApiKey(), + ]); + + return Command::SUCCESS; + } +} diff --git a/src/Entity/Server.php b/src/Entity/Server.php index cdf15b15..8786b19e 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -5,6 +5,7 @@ namespace App\Entity; use App\Repository\ServerRepository; +use App\Trait\ApiKeyEntityTrait; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -16,16 +17,9 @@ #[ORM\Entity(repositoryClass: ServerRepository::class)] class Server extends AbstractBaseEntity implements UserInterface, \Stringable { - private const array ROLES = ['ROLE_USER', 'ROLE_SERVER']; + use ApiKeyEntityTrait; - #[ORM\Column(type: 'string', length: 255, unique: true)] - #[Assert\Length( - min: 40, - max: 255, - minMessage: 'Api key must be at least {{ limit }} characters long', - maxMessage: 'Api key cannot be longer than {{ limit }} characters', - )] - private string $apiKey; + private const array ROLES = ['ROLE_USER', 'ROLE_SERVER']; #[ORM\Column(type: 'string', length: 255, unique: true)] #[Groups(['export'])] @@ -93,8 +87,8 @@ class Server extends AbstractBaseEntity implements UserInterface, \Stringable */ public function __construct() { + $this->setApiKey($this->generateApiKey()); $this->detectionResults = new ArrayCollection(); - $this->apiKey = sha1(\random_bytes(40)); $this->installations = new ArrayCollection(); } @@ -258,18 +252,6 @@ public function setUsedFor(?string $usedFor): self return $this; } - public function getApiKey(): string - { - return $this->apiKey; - } - - public function setApiKey(string $apiKey): self - { - $this->apiKey = $apiKey; - - return $this; - } - /** * @return Collection */ diff --git a/src/Entity/User.php b/src/Entity/User.php index 70ad5369..15de1c2a 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -5,12 +5,15 @@ namespace App\Entity; use App\Repository\UserRepository; +use App\Trait\ApiKeyEntityTrait; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Entity(repositoryClass: UserRepository::class)] class User extends AbstractBaseEntity implements UserInterface { + use ApiKeyEntityTrait; + public function __construct( #[ORM\Column(length: 255)] private string $name, @@ -19,6 +22,7 @@ public function __construct( #[ORM\Column(type: 'json')] private array $roles = [], ) { + $this->setApiKey($this->generateApiKey()); } #[\Override] diff --git a/src/Security/ApiKeyAuthenticator.php b/src/Security/ApiKeyAuthenticator.php index 8232e27f..f12e67ed 100644 --- a/src/Security/ApiKeyAuthenticator.php +++ b/src/Security/ApiKeyAuthenticator.php @@ -4,11 +4,14 @@ namespace App\Security; +use App\Repository\ServerRepository; +use App\Repository\UserRepository; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; @@ -20,6 +23,12 @@ class ApiKeyAuthenticator extends AbstractAuthenticator public const string AUTH_HEADER = 'Authorization'; public const string AUTH_HEADER_PREFIX = 'Apikey '; + public function __construct( + private readonly ServerRepository $serverRepository, + private readonly UserRepository $userRepository, + ) { + } + /** * Called on every request to decide if this authenticator should be used for the request. * @@ -40,7 +49,14 @@ public function authenticate(Request $request): Passport throw new CustomUserMessageAuthenticationException('No API token provided'); } - return new SelfValidatingPassport(new UserBadge($apiKey)); + // Users and servers can authenticate to use the API. + $apiUser = $this->serverRepository->findOneBy(['apiKey' => $apiKey]) + ?? $this->userRepository->findOneBy(['apiKey' => $apiKey]); + if (null !== $apiUser) { + return new SelfValidatingPassport(new UserBadge($apiUser->getUserIdentifier())); + } + + throw new BadCredentialsException('Invalid credentials.'); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response diff --git a/src/Trait/ApiKeyEntityTrait.php b/src/Trait/ApiKeyEntityTrait.php new file mode 100644 index 00000000..7c65c811 --- /dev/null +++ b/src/Trait/ApiKeyEntityTrait.php @@ -0,0 +1,35 @@ +apiKey; + } + + public function setApiKey(string $apiKey): static + { + $this->apiKey = $apiKey; + + return $this; + } + + public function generateApiKey(): string + { + return sha1(\random_bytes(40)); + } +} From 5b72353f42eea2876e73de5f9f62ac9d12626c34 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 2 Jul 2026 15:55:30 +0200 Subject: [PATCH 2/7] Added security to the detection result API --- CHANGELOG.md | 1 + src/Entity/DetectionResult.php | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 924edfbf..c6134248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#88](https://github.com/itk-dev/devops_itksites/pull/88) - Let users use the API + - Add security to detection results API endpoint - [#80](https://github.com/itk-dev/devops_itksites/pull/80) 5566: Service agreements - Add security contract entity with crud controller - Add Abstract full crud controller and extend on it in some cases diff --git a/src/Entity/DetectionResult.php b/src/Entity/DetectionResult.php index d47821fa..00769cd8 100644 --- a/src/Entity/DetectionResult.php +++ b/src/Entity/DetectionResult.php @@ -17,6 +17,7 @@ #[ApiResource( operations: [ new Post( + security: "is_granted('ROLE_SERVER')", status: 202, output: false, messenger: true, From 895394048910911c8d2a2e58409186668b789d9e Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 2 Jul 2026 15:56:01 +0200 Subject: [PATCH 3/7] Added API endpoints for server and site collections --- CHANGELOG.md | 1 + src/Entity/Server.php | 7 +++++++ src/Entity/Site.php | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6134248..ff105151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#88](https://github.com/itk-dev/devops_itksites/pull/88) - Let users use the API - Add security to detection results API endpoint + - Add server and site collections API endpoints - [#80](https://github.com/itk-dev/devops_itksites/pull/80) 5566: Service agreements - Add security contract entity with crud controller - Add Abstract full crud controller and extend on it in some cases diff --git a/src/Entity/Server.php b/src/Entity/Server.php index 8786b19e..6b27d673 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -4,6 +4,8 @@ namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; use App\Repository\ServerRepository; use App\Trait\ApiKeyEntityTrait; use Doctrine\Common\Collections\ArrayCollection; @@ -14,6 +16,11 @@ use Symfony\Component\Serializer\Attribute\SerializedName; use Symfony\Component\Validator\Constraints as Assert; +#[ApiResource( + normalizationContext: ['groups' => ['export']], + security: "is_granted('ROLE_USER')", +)] +#[GetCollection()] #[ORM\Entity(repositoryClass: ServerRepository::class)] class Server extends AbstractBaseEntity implements UserInterface, \Stringable { diff --git a/src/Entity/Site.php b/src/Entity/Site.php index dbfd6c7a..9c52cf9d 100644 --- a/src/Entity/Site.php +++ b/src/Entity/Site.php @@ -4,6 +4,8 @@ namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; use App\Repository\SiteRepository; use App\Types\SiteType; use Doctrine\Common\Collections\ArrayCollection; @@ -13,6 +15,11 @@ use Symfony\Component\Serializer\Attribute\SerializedName; use Symfony\Component\Validator\Constraints as Assert; +#[ApiResource( + normalizationContext: ['groups' => ['export']], + security: "is_granted('ROLE_USER')", +)] +#[GetCollection()] #[ORM\Entity(repositoryClass: SiteRepository::class)] #[ORM\UniqueConstraint(name: 'server_rootDir_configFilePath_idx', fields: ['server', 'rootDir', 'configFilePath'])] class Site extends AbstractHandlerResult implements \Stringable From 94aa0ec73ac9df05894a1bb0e3168846df007905 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 2 Jul 2026 15:56:16 +0200 Subject: [PATCH 4/7] Cleaned up user fixture. Added another user. --- fixtures/user.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fixtures/user.yaml b/fixtures/user.yaml index de04a82f..12605096 100644 --- a/fixtures/user.yaml +++ b/fixtures/user.yaml @@ -1,3 +1,5 @@ App\Entity\User: user_admin: - __construct: ["admin@example.com", "Admin", [ROLE_ADMIN]] + __construct: ["Admin", "admin@example.com", [ROLE_ADMIN]] + user_user: + __construct: ["User", "user@example.com", [ROLE_USER]] From e2cf897e16b8ee99d4efcb5cc1f488ef135d89e8 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 2 Jul 2026 16:06:17 +0200 Subject: [PATCH 5/7] Updated API spec --- public/api-spec-v1.json | 183 ++++++++++++++++++++++++++++++++++++++++ public/api-spec-v1.yaml | 122 +++++++++++++++++++++++++++ 2 files changed, 305 insertions(+) diff --git a/public/api-spec-v1.json b/public/api-spec-v1.json index 47ce5887..8b60c775 100644 --- a/public/api-spec-v1.json +++ b/public/api-spec-v1.json @@ -92,6 +92,144 @@ "required": true } } + }, + "/api/servers": { + "get": { + "operationId": "api_servers_get_collection", + "tags": [ + "Server" + ], + "responses": { + "200": { + "description": "Server collection", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Server-export" + } + } + }, + "application/ld+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Server-export" + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/ld+json": { + "schema": { + "$ref": "#/components/schemas/Error.jsonld" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "links": {} + } + }, + "summary": "Retrieves the collection of Server resources.", + "description": "Retrieves the collection of Server resources.", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "The collection page number", + "required": false, + "deprecated": false, + "schema": { + "type": "integer", + "default": 1 + }, + "style": "form", + "explode": true + } + ] + } + }, + "/api/sites": { + "get": { + "operationId": "api_sites_get_collection", + "tags": [ + "Site" + ], + "responses": { + "200": { + "description": "Site collection", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Site-export" + } + } + }, + "application/ld+json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Site-export" + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/ld+json": { + "schema": { + "$ref": "#/components/schemas/Error.jsonld" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "links": {} + } + }, + "summary": "Retrieves the collection of Site resources.", + "description": "Retrieves the collection of Site resources.", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "The collection page number", + "required": false, + "deprecated": false, + "schema": { + "type": "integer", + "default": 1 + }, + "style": "form", + "explode": true + } + ] + } } }, "components": { @@ -353,6 +491,43 @@ ] } } + }, + "Server-export": { + "type": "object", + "properties": { + "Name": { + "default": "", + "type": "string" + } + } + }, + "Site-export": { + "type": "object", + "required": [ + "PHP version" + ], + "properties": { + "PHP version": { + "minLength": 1, + "maxLength": 10, + "default": "", + "type": "string" + }, + "Primary domain": { + "readOnly": true, + "type": "string" + }, + "Type": { + "default": "", + "type": "string" + }, + "Server": { + "$ref": "#/components/schemas/Server-export" + }, + "rootDir": { + "type": "string" + } + } } }, "responses": {}, @@ -378,6 +553,14 @@ { "name": "DetectionResult", "description": "Resource 'DetectionResult' operations." + }, + { + "name": "Server", + "description": "Resource 'Server' operations." + }, + { + "name": "Site", + "description": "Resource 'Site' operations." } ], "webhooks": {} diff --git a/public/api-spec-v1.yaml b/public/api-spec-v1.yaml index d2a01794..e9be5b35 100755 --- a/public/api-spec-v1.yaml +++ b/public/api-spec-v1.yaml @@ -59,6 +59,98 @@ paths: schema: $ref: '#/components/schemas/DetectionResult-write' required: true + /api/servers: + get: + operationId: api_servers_get_collection + tags: + - Server + responses: + '200': + description: 'Server collection' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Server-export' + application/ld+json: + schema: + type: array + items: + $ref: '#/components/schemas/Server-export' + '403': + description: Forbidden + content: + application/ld+json: + schema: + $ref: '#/components/schemas/Error.jsonld' + application/problem+json: + schema: + $ref: '#/components/schemas/Error' + application/json: + schema: + $ref: '#/components/schemas/Error' + links: {} + summary: 'Retrieves the collection of Server resources.' + description: 'Retrieves the collection of Server resources.' + parameters: + - + name: page + in: query + description: 'The collection page number' + required: false + deprecated: false + schema: + type: integer + default: 1 + style: form + explode: true + /api/sites: + get: + operationId: api_sites_get_collection + tags: + - Site + responses: + '200': + description: 'Site collection' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Site-export' + application/ld+json: + schema: + type: array + items: + $ref: '#/components/schemas/Site-export' + '403': + description: Forbidden + content: + application/ld+json: + schema: + $ref: '#/components/schemas/Error.jsonld' + application/problem+json: + schema: + $ref: '#/components/schemas/Error' + application/json: + schema: + $ref: '#/components/schemas/Error' + links: {} + summary: 'Retrieves the collection of Site resources.' + description: 'Retrieves the collection of Site resources.' + parameters: + - + name: page + in: query + description: 'The collection page number' + required: false + deprecated: false + schema: + type: integer + default: 1 + style: form + explode: true components: schemas: ConstraintViolation: @@ -250,6 +342,32 @@ components: type: - string - 'null' + Server-export: + type: object + properties: + Name: + default: '' + type: string + Site-export: + type: object + required: + - 'PHP version' + properties: + 'PHP version': + minLength: 1 + maxLength: 10 + default: '' + type: string + 'Primary domain': + readOnly: true + type: string + Type: + default: '' + type: string + Server: + $ref: '#/components/schemas/Server-export' + rootDir: + type: string responses: {} parameters: {} examples: {} @@ -267,4 +385,8 @@ security: tags: - name: DetectionResult description: "Resource 'DetectionResult' operations." + - name: Server + description: "Resource 'Server' operations." + - name: Site + description: "Resource 'Site' operations." webhooks: {} From 813a912c648751a1bcccbe1cd9ae710652db6952 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Thu, 2 Jul 2026 16:28:17 +0200 Subject: [PATCH 6/7] Added console command test --- src/Command/UserSetApiKeyCommand.php | 5 +- tests/Command/UserSetApiKeyCommandTest.php | 67 ++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/Command/UserSetApiKeyCommandTest.php diff --git a/src/Command/UserSetApiKeyCommand.php b/src/Command/UserSetApiKeyCommand.php index 8a0bd311..e7926621 100644 --- a/src/Command/UserSetApiKeyCommand.php +++ b/src/Command/UserSetApiKeyCommand.php @@ -7,7 +7,6 @@ use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( @@ -31,7 +30,9 @@ public function __invoke( ?? $this->userRepository->findOneBy(['name' => $userId]); if (null === $user) { - throw new InvalidArgumentException(sprintf('Cannot load user with id %s', $userId)); + $io->error(sprintf('Cannot load user with id %s', $userId)); + + return Command::INVALID; } $question = sprintf('Really set API key on user %s', $user->getUserIdentifier()); diff --git a/tests/Command/UserSetApiKeyCommandTest.php b/tests/Command/UserSetApiKeyCommandTest.php new file mode 100644 index 00000000..3f782897 --- /dev/null +++ b/tests/Command/UserSetApiKeyCommandTest.php @@ -0,0 +1,67 @@ +setAutoExit(false); + + $applicationTester = new ApplicationTester($application); + $applicationTester->run([ + 'command' => 'app:user:set-api-key', + '--help' => true, + ]); + $applicationTester->assertCommandIsSuccessful(); + + // Missing user ID argument. + $applicationTester->run([ + 'command' => 'app:user:set-api-key', + ]); + $applicationTester->assertCommandFailed(); + + // Invalid user ID argument. + $applicationTester->run([ + 'command' => 'app:user:set-api-key', + 'user-id' => 'this-user-does-no-exist', + ]); + $applicationTester->assertCommandIsInvalid(); + $output = $applicationTester->getDisplay(); + $this->assertStringContainsString('Cannot load user with id this-user-does-no-exist', $output); + + // Valid user ID (name). + $applicationTester->run([ + 'command' => 'app:user:set-api-key', + 'user-id' => 'admin', + '--no-interaction' => true, + ]); + $applicationTester->assertCommandIsSuccessful(); + + $output = $applicationTester->getDisplay(); + $this->assertStringContainsString('API key for user admin@example.com set to', $output); + + // Valid user ID (email). + $applicationTester->run([ + 'command' => 'app:user:set-api-key', + 'user-id' => 'admin@example.com', + '--no-interaction' => true, + ]); + $applicationTester->assertCommandIsSuccessful(); + + $output = $applicationTester->getDisplay(); + $this->assertStringContainsString('API key for user admin@example.com set to', $output); + } +} From b89bc6d9131b972cb53bd8594becd99ed24606b7 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 3 Jul 2026 08:54:38 +0200 Subject: [PATCH 7/7] Updated API name and description --- config/packages/api_platform.yaml | 7 +++++-- public/api-spec-v1.json | 4 ++-- public/api-spec-v1.yaml | 7 +++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 6651fbaa..387fd872 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -1,6 +1,9 @@ api_platform: - title: 'ITKsites Detection API' - description: 'REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.' + title: 'ITKsites API' + description: | + REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories. + + The API also provides read-only access to information on servers and sites created by processing detection results. version: '1.0.0' mapping: diff --git a/public/api-spec-v1.json b/public/api-spec-v1.json index 8b60c775..bc5923d6 100644 --- a/public/api-spec-v1.json +++ b/public/api-spec-v1.json @@ -1,8 +1,8 @@ { "openapi": "3.1.0", "info": { - "title": "ITKsites Detection API", - "description": "REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.", + "title": "ITKsites API", + "description": "REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.\n\nThe API also provides read-only access to information on servers and sites created by processing detection results.", "version": "1.0.0" }, "servers": [ diff --git a/public/api-spec-v1.yaml b/public/api-spec-v1.yaml index e9be5b35..cdf73060 100755 --- a/public/api-spec-v1.yaml +++ b/public/api-spec-v1.yaml @@ -1,7 +1,10 @@ openapi: 3.1.0 info: - title: 'ITKsites Detection API' - description: 'REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.' + title: 'ITKsites API' + description: |- + REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories. + + The API also provides read-only access to information on servers and sites created by processing detection results. version: 1.0.0 servers: - url: 'https://itksites.local.itkdev.dk'