diff --git a/tests/Fixture/AuthKeysFixture.php b/tests/Fixture/AuthKeysFixture.php index 802ca5c..24034f3 100644 --- a/tests/Fixture/AuthKeysFixture.php +++ b/tests/Fixture/AuthKeysFixture.php @@ -11,9 +11,18 @@ class AuthKeysFixture extends TestFixture { public $connection = 'test'; + public const ADMIN_API_ID = 1; public const ADMIN_API_KEY = 'd033e22ae348aeb5660fc2140aec35850c4da997'; + + + public const SYNC_API_ID = 2; public const SYNC_API_KEY = '6b387ced110858dcbcda36edb044dc18f91a0894'; + + + public const ORG_ADMIN_API_ID = 3; public const ORG_ADMIN_API_KEY = '1c4685d281d478dbcebd494158024bc3539004d0'; + + public const REGULAR_USER_API_ID = 4; public const REGULAR_USER_API_KEY = '12dea96fec20593566ab75692c9949596833adc9'; public function init(): void @@ -23,6 +32,7 @@ class AuthKeysFixture extends TestFixture $this->records = [ [ + 'id' => self::ADMIN_API_ID, 'uuid' => $faker->uuid(), 'authkey' => $hasher->hash(self::ADMIN_API_KEY), 'authkey_start' => substr(self::ADMIN_API_KEY, 0, 4), @@ -34,6 +44,7 @@ class AuthKeysFixture extends TestFixture 'modified' => $faker->dateTime()->getTimestamp() ], [ + 'id' => self::SYNC_API_ID, 'uuid' => $faker->uuid(), 'authkey' => $hasher->hash(self::SYNC_API_KEY), 'authkey_start' => substr(self::SYNC_API_KEY, 0, 4), @@ -45,6 +56,7 @@ class AuthKeysFixture extends TestFixture 'modified' => $faker->dateTime()->getTimestamp() ], [ + 'id' => self::ORG_ADMIN_API_ID, 'uuid' => $faker->uuid(), 'authkey' => $hasher->hash(self::ORG_ADMIN_API_KEY), 'authkey_start' => substr(self::ORG_ADMIN_API_KEY, 0, 4), @@ -56,6 +68,7 @@ class AuthKeysFixture extends TestFixture 'modified' => $faker->dateTime()->getTimestamp() ], [ + 'id' => self::REGULAR_USER_API_ID, 'uuid' => $faker->uuid(), 'authkey' => $hasher->hash(self::REGULAR_USER_API_KEY), 'authkey_start' => substr(self::REGULAR_USER_API_KEY, 0, 4), diff --git a/tests/TestCase/Api/AuthKeys/AddAuthKeyApiTest.php b/tests/TestCase/Api/AuthKeys/AddAuthKeyApiTest.php new file mode 100644 index 0000000..810dad2 --- /dev/null +++ b/tests/TestCase/Api/AuthKeys/AddAuthKeyApiTest.php @@ -0,0 +1,78 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'authkey' => $faker->sha1, + 'expiration' => 0, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'comment' => $faker->text + ] + ); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"uuid": "%s"', $uuid)); + $this->assertDbRecordExists('AuthKeys', ['uuid' => $uuid]); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec(self::ENDPOINT, 'post'); + } + + public function testAddAdminAuthKeyNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + + $faker = \Faker\Factory::create(); + $uuid = $faker->uuid; + + + $this->post( + self::ENDPOINT, + [ + 'uuid' => $uuid, + 'authkey' => $faker->sha1, + 'expiration' => 0, + 'user_id' => UsersFixture::USER_ADMIN_ID, + 'comment' => $faker->text + ] + ); + + $this->assertResponseCode(404); + $this->addWarning('Should return 405 Method Not Allowed instead of 404 Not Found'); + $this->assertDbRecordNotExists('AuthKeys', ['uuid' => $uuid]); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec(self::ENDPOINT, 'post'); + } +} diff --git a/tests/TestCase/Api/AuthKeys/DeleteAuthKeyApiTest.php b/tests/TestCase/Api/AuthKeys/DeleteAuthKeyApiTest.php new file mode 100644 index 0000000..21a22f6 --- /dev/null +++ b/tests/TestCase/Api/AuthKeys/DeleteAuthKeyApiTest.php @@ -0,0 +1,51 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, AuthKeysFixture::ADMIN_API_ID); + $this->delete($url); + + $this->assertResponseOk(); + $this->assertDbRecordNotExists('AuthKeys', ['id' => AuthKeysFixture::ADMIN_API_ID]); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'delete'); + } + + public function testDeleteOrgAdminAuthKeyNotAllowedAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, AuthKeysFixture::ORG_ADMIN_API_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $this->assertDbRecordExists('AuthKeys', ['id' => AuthKeysFixture::ORG_ADMIN_API_ID]); + //TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec($url, 'delete'); + } +} diff --git a/tests/TestCase/Api/AuthKeys/IndexAuthKeysApiTest.php b/tests/TestCase/Api/AuthKeys/IndexAuthKeysApiTest.php new file mode 100644 index 0000000..9fbe3e6 --- /dev/null +++ b/tests/TestCase/Api/AuthKeys/IndexAuthKeysApiTest.php @@ -0,0 +1,48 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', AuthKeysFixture::ADMIN_API_ID)); + // TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec(self::ENDPOINT); + } + + public function testIndexDoesNotShowAdminAuthKeysAsRegularUser(): void + { + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseNotContains(sprintf('"id": %d', AuthKeysFixture::REGULAR_USER_API_KEY)); + // TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec(self::ENDPOINT); + } +} diff --git a/tests/TestCase/Api/Broods/DeleteBroodsApiTest.php b/tests/TestCase/Api/Broods/DeleteBroodApiTest.php similarity index 83% rename from tests/TestCase/Api/Broods/DeleteBroodsApiTest.php rename to tests/TestCase/Api/Broods/DeleteBroodApiTest.php index 94aa0a8..1b90bd6 100644 --- a/tests/TestCase/Api/Broods/DeleteBroodsApiTest.php +++ b/tests/TestCase/Api/Broods/DeleteBroodApiTest.php @@ -10,7 +10,7 @@ use App\Test\Fixture\AuthKeysFixture; use App\Test\Fixture\BroodsFixture; use App\Test\Helper\ApiTestTrait; -class DeleteBroodsApiTest extends TestCase +class DeleteBroodApiTest extends TestCase { use IntegrationTestTrait; use ApiTestTrait; @@ -36,7 +36,6 @@ class DeleteBroodsApiTest extends TestCase $this->assertDbRecordNotExists('Broods', ['id' => BroodsFixture::BROOD_A_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } public function testDeleteBroodNotAllowedAsRegularUser(): void @@ -49,6 +48,5 @@ class DeleteBroodsApiTest extends TestCase $this->assertDbRecordExists('Broods', ['id' => BroodsFixture::BROOD_A_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } } diff --git a/tests/TestCase/Api/EncryptionKeys/DeleteEncryptionKeyApiTest.php b/tests/TestCase/Api/EncryptionKeys/DeleteEncryptionKeyApiTest.php index 934d3e7..02ecd25 100644 --- a/tests/TestCase/Api/EncryptionKeys/DeleteEncryptionKeyApiTest.php +++ b/tests/TestCase/Api/EncryptionKeys/DeleteEncryptionKeyApiTest.php @@ -36,7 +36,6 @@ class DeleteEncryptionKeyApiTest extends TestCase $this->assertDbRecordNotExists('EncryptionKeys', ['id' => EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } public function testDeleteEncryptionKeyNotAllowedAsRegularUser(): void @@ -49,6 +48,5 @@ class DeleteEncryptionKeyApiTest extends TestCase $this->assertDbRecordExists('EncryptionKeys', ['id' => EncryptionKeysFixture::ENCRYPTION_KEY_ORG_B_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } } diff --git a/tests/TestCase/Api/Individuals/DeleteIndividualApiTest.php b/tests/TestCase/Api/Individuals/DeleteIndividualApiTest.php index e50cf63..32b4ba7 100644 --- a/tests/TestCase/Api/Individuals/DeleteIndividualApiTest.php +++ b/tests/TestCase/Api/Individuals/DeleteIndividualApiTest.php @@ -35,7 +35,6 @@ class DeleteIndividualApiTest extends TestCase $this->assertDbRecordNotExists('Individuals', ['id' => IndividualsFixture::INDIVIDUAL_A_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } public function testDeleteIndividualNotAllowedAsRegularUser(): void @@ -48,6 +47,5 @@ class DeleteIndividualApiTest extends TestCase $this->assertDbRecordExists('Individuals', ['id' => IndividualsFixture::INDIVIDUAL_ADMIN_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } } diff --git a/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php b/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php index 6a323fb..12b62d1 100644 --- a/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php +++ b/tests/TestCase/Api/Organisations/DeleteOrganisationApiTest.php @@ -35,7 +35,6 @@ class DeleteOrganisationApiTest extends TestCase $this->assertDbRecordNotExists('Organisations', ['id' => OrganisationsFixture::ORGANISATION_B_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } public function testDeleteOrganisationNotAllowedAsRegularUser(): void @@ -48,6 +47,5 @@ class DeleteOrganisationApiTest extends TestCase $this->assertDbRecordExists('Organisations', ['id' => OrganisationsFixture::ORGANISATION_B_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } } diff --git a/tests/TestCase/Api/SharingGroups/DeleteSharingGroupApiTest.php b/tests/TestCase/Api/SharingGroups/DeleteSharingGroupApiTest.php index c93bb05..36379d4 100644 --- a/tests/TestCase/Api/SharingGroups/DeleteSharingGroupApiTest.php +++ b/tests/TestCase/Api/SharingGroups/DeleteSharingGroupApiTest.php @@ -36,7 +36,6 @@ class DeleteSharingGroupApiTest extends TestCase $this->assertDbRecordNotExists('SharingGroups', ['id' => SharingGroupsFixture::SHARING_GROUP_A_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } public function testDeleteSharingGroupNotAllowedAsRegularUser(): void @@ -49,6 +48,5 @@ class DeleteSharingGroupApiTest extends TestCase $this->assertDbRecordExists('SharingGroups', ['id' => SharingGroupsFixture::SHARING_GROUP_A_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } } diff --git a/tests/TestCase/Api/Users/DeleteUserApiTest.php b/tests/TestCase/Api/Users/DeleteUserApiTest.php index 50df2e5..48f9069 100644 --- a/tests/TestCase/Api/Users/DeleteUserApiTest.php +++ b/tests/TestCase/Api/Users/DeleteUserApiTest.php @@ -37,7 +37,6 @@ class DeleteUserApiTest extends TestCase $this->assertDbRecordNotExists('Users', ['id' => UsersFixture::USER_REGULAR_USER_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } public function testDeleteUserNotAllowedAsRegularUser(): void @@ -50,6 +49,5 @@ class DeleteUserApiTest extends TestCase $this->assertDbRecordExists('Users', ['id' => UsersFixture::USER_ORG_ADMIN_ID]); //TODO: $this->assertRequestMatchesOpenApiSpec(); $this->assertResponseMatchesOpenApiSpec($url, 'delete'); - $this->addWarning('TODO: CRUDComponent::delete() sets some view variables, does not take into account `isRest()`, fix it.'); } } diff --git a/tests/TestCase/Api/Users/EditUserApiTest.php b/tests/TestCase/Api/Users/EditUserApiTest.php index 8d2810d..39673ee 100644 --- a/tests/TestCase/Api/Users/EditUserApiTest.php +++ b/tests/TestCase/Api/Users/EditUserApiTest.php @@ -57,6 +57,7 @@ class EditUserApiTest extends TestCase ] ); + $this->assertResponseOk(); $this->assertDbRecordNotExists('Users', [ 'id' => UsersFixture::USER_REGULAR_USER_ID, 'role_id' => RolesFixture::ROLE_ADMIN_ID @@ -75,6 +76,7 @@ class EditUserApiTest extends TestCase ] ); + $this->assertResponseOk(); $this->assertDbRecordExists('Users', [ 'id' => UsersFixture::USER_REGULAR_USER_ID, 'username' => 'test' diff --git a/webroot/docs/openapi.yaml b/webroot/docs/openapi.yaml index d293ed5..076af9f 100644 --- a/webroot/docs/openapi.yaml +++ b/webroot/docs/openapi.yaml @@ -25,6 +25,8 @@ tags: description: "Cerebrate can connect to other Cerebrate instances to exchange trust information and to instrument interconnectivity between connected local tools. Each such Cerebrate instance with its connected tools is considered to be a brood." - name: EncryptionKeys description: "Assign encryption keys to the user, used to securely communicate or validate messages coming from the user." + - name: AuthKeys + description: "Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier." paths: /api/v1/individuals/index: @@ -726,6 +728,61 @@ paths: default: $ref: "#/components/responses/ApiErrorResponse" + # AuthKeys + /api/v1/authKeys/index: + get: + summary: "Get auth keys list" + operationId: getAuthKeys + tags: + - AuthKeys + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/AuthKeyListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/authKeys/add: + post: + summary: "Add auth keys" + operationId: addAuthKey + tags: + - AuthKeys + requestBody: + $ref: "#/components/requestBodies/CreateAuthKeyRequest" + responses: + "200": + $ref: "#/components/responses/AuthKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/authKeys/delete/{authKeyId}: + delete: + summary: "Delete auth key by ID" + operationId: deleteAuthKeyById + tags: + - AuthKeys + parameters: + - $ref: "#/components/parameters/authKeyId" + responses: + "200": + $ref: "#/components/responses/AuthKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + components: schemas: # General @@ -750,9 +807,6 @@ components: format: email example: "user@example.com" - AuthKey: - type: string - ModelName: type: string enum: @@ -1195,7 +1249,7 @@ components: skip_proxy: type: boolean authkey: - $ref: "#/components/schemas/AuthKey" + $ref: "#/components/schemas/AuthKeyRaw" organisation: $ref: "#/components/schemas/Organisation" created: @@ -1224,7 +1278,7 @@ components: EncryptionKeyExpiration: type: integer - description: "Timestamp or null of there is no expiration" + description: "UNIX timestamp or null of there is no expiration" nullable: true EncryptionKey: @@ -1256,6 +1310,58 @@ components: items: $ref: "#/components/schemas/EncryptionKey" + # AuthKeys + AuthKeyRaw: + type: string + + AuthKeyHashed: + type: string + + AuthKeyExpiration: + type: integer + description: "0 or UNIX timestamp" + example: 0 + + AuthKeyCreatedAt: + type: integer + description: "UNIX timestamp" + + AuthKeyComment: + type: string + + AuthKey: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + authkey: + $ref: "#/components/schemas/AuthKeyHashed" + authkey_start: + type: string + example: abcd + authkey_end: + type: string + example: abcd + created: + $ref: "#/components/schemas/AuthKeyCreatedAt" + expiration: + $ref: "#/components/schemas/AuthKeyExpiration" + type: integer + description: "0 or UNIX timestamp" + user_id: + $ref: "#/components/schemas/ID" + comment: + $ref: "#/components/schemas/AuthKeyComment" + user: + $ref: "#/components/schemas/User" + + AuthKeyList: + type: array + items: + $ref: "#/components/schemas/AuthKey" + # Errors ApiError: type: object @@ -1373,6 +1479,14 @@ components: schema: $ref: "#/components/schemas/ID" + authKeyId: + name: authKeyId + in: path + description: "Numeric ID of the AuthKey" + required: true + schema: + $ref: "#/components/schemas/ID" + quickFilter: name: quickFilter in: query @@ -1629,7 +1743,7 @@ components: skip_proxy: type: boolean authkey: - $ref: "#/components/schemas/AuthKey" + $ref: "#/components/schemas/AuthKeyRaw" EditBroodRequest: required: true @@ -1655,7 +1769,7 @@ components: skip_proxy: type: boolean authkey: - $ref: "#/components/schemas/AuthKey" + $ref: "#/components/schemas/AuthKeyRaw" CreateEncryptionKeyRequest: required: true @@ -1701,6 +1815,25 @@ components: owner_model: $ref: "#/components/schemas/ModelName" + # AuthKeys + CreateAuthKeyRequest: + required: true + content: + application/json: + schema: + type: object + properties: + uuid: + $ref: "#/components/schemas/UUID" + authkey: + $ref: "#/components/schemas/AuthKeyRaw" + expiration: + $ref: "#/components/schemas/AuthKeyExpiration" + user_id: + $ref: "#/components/schemas/ID" + comment: + $ref: "#/components/schemas/AuthKeyComment" + responses: # Individuals IndividualResponse: @@ -1881,6 +2014,21 @@ components: schema: $ref: "#/components/schemas/EncryptionKeyList" + # AuthKeys + AuthKeyResponse: + description: "Auth key response" + content: + application/json: + schema: + $ref: "#/components/schemas/AuthKey" + + AuthKeyListResponse: + description: "Auth key list response" + content: + application/json: + schema: + $ref: "#/components/schemas/AuthKeyList" + # Errors ApiErrorResponse: description: "Unexpected API error"