diff --git a/tests/Fixture/EncryptionKeysFixture.php b/tests/Fixture/EncryptionKeysFixture.php new file mode 100644 index 0000000..05b92f8 --- /dev/null +++ b/tests/Fixture/EncryptionKeysFixture.php @@ -0,0 +1,133 @@ +records = [ + [ + 'id' => self::ENCRYPTION_KEY_ORG_A_ID, + 'uuid' => $faker->uuid(), + 'type' => self::TYPE_PGP, + 'encryption_key' => $this->getPublicKey(self::KEY_TYPE_EDCH), + 'revoked' => false, + 'expires' => null, + 'owner_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'owner_model' => 'Organisation', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + [ + 'id' => self::ENCRYPTION_KEY_ORG_B_ID, + 'uuid' => $faker->uuid(), + 'type' => self::TYPE_PGP, + 'encryption_key' => $this->getPublicKey(self::KEY_TYPE_EDCH), + 'revoked' => false, + 'expires' => null, + 'owner_id' => OrganisationsFixture::ORGANISATION_A_ID, + 'owner_model' => 'Organisation', + 'created' => $faker->dateTime()->getTimestamp(), + 'modified' => $faker->dateTime()->getTimestamp() + ], + ]; + parent::init(); + } + + public function getPublicKey(string $type): string + { + switch ($type) { + case self::KEY_TYPE_EDCH: + return <<setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID); + $this->delete($url); + + $this->assertResponseOk(); + $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 + { + $this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY); + $url = sprintf('%s/%d', self::ENDPOINT, EncryptionKeysFixture::ENCRYPTION_KEY_ORG_B_ID); + $this->delete($url); + + $this->assertResponseCode(405); + $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/EncryptionKeys/IndexEncryptionKeysApiTest.php b/tests/TestCase/Api/EncryptionKeys/IndexEncryptionKeysApiTest.php new file mode 100644 index 0000000..0b7cb98 --- /dev/null +++ b/tests/TestCase/Api/EncryptionKeys/IndexEncryptionKeysApiTest.php @@ -0,0 +1,40 @@ +setAuthToken(AuthKeysFixture::ADMIN_API_KEY); + $this->get(self::ENDPOINT); + + $this->assertResponseOk(); + $this->assertResponseContains(sprintf('"id": %d', EncryptionKeysFixture::ENCRYPTION_KEY_ORG_A_ID)); + // TODO: $this->assertRequestMatchesOpenApiSpec(); + $this->assertResponseMatchesOpenApiSpec(self::ENDPOINT); + } +} diff --git a/webroot/docs/openapi.yaml b/webroot/docs/openapi.yaml index f9418b9..c4852eb 100644 --- a/webroot/docs/openapi.yaml +++ b/webroot/docs/openapi.yaml @@ -23,6 +23,8 @@ tags: description: "Sharing groups are distribution lists usable by tools that can exchange information with a list of trusted partners. Create recurring or ad hoc sharing groups and share them with the members of the sharing group." - name: Broods 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." paths: /api/v1/individuals/index: @@ -575,7 +577,7 @@ paths: default: $ref: "#/components/responses/ApiErrorResponse" - /api/v1/broods/edit/{sharingGroupId}: + /api/v1/broods/edit/{broodId}: put: summary: "Edit brood" operationId: editBrood @@ -631,6 +633,43 @@ paths: default: $ref: "#/components/responses/ApiErrorResponse" + # EncryptionKeys + /api/v1/encryptionKeys/index: + get: + summary: "Get encryption keys list" + operationId: getEncryptionKeys + tags: + - EncryptionKeys + parameters: + - $ref: "#/components/parameters/quickFilter" + responses: + "200": + $ref: "#/components/responses/EncryptionKeyListResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + + /api/v1/encryptionKeys/delete/{encryptionKeyId}: + delete: + summary: "Delete encryption key by ID" + operationId: deleteEncryptionKeyById + tags: + - EncryptionKeys + parameters: + - $ref: "#/components/parameters/encryptionKeyId" + responses: + "200": + $ref: "#/components/responses/EncryptionKeyResponse" + "403": + $ref: "#/components/responses/UnauthorizedApiErrorResponse" + "405": + $ref: "#/components/responses/MethodNotAllowedApiErrorResponse" + default: + $ref: "#/components/responses/ApiErrorResponse" + components: schemas: # General @@ -658,6 +697,18 @@ components: AuthKey: type: string + ModelName: + type: string + enum: + - "Organisation" + - "User" + - "Individual" + - "EncryptionKey" + - "Role" + - "Tag" + - "SharingGroup" + - "Brood" + # Individuals IndividualFirstName: type: string @@ -1089,18 +1140,66 @@ components: type: boolean authkey: $ref: "#/components/schemas/AuthKey" + organisation: + $ref: "#/components/schemas/Organisation" created: $ref: "#/components/schemas/DateTime" modified: $ref: "#/components/schemas/DateTime" - organisation: - $ref: "#/components/schemas/Organisation" BroodList: type: array items: $ref: "#/components/schemas/Brood" + # EncryptionKeys + EncryptionKeyType: + type: string + enum: + - "pgp" + - "smime" + + EncryptionKeyValue: + type: string + example: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + ... + -----END PGP PUBLIC KEY BLOCK----- + + EncryptionKeyExpiration: + type: integer + description: "Timestamp or null of there is no expiration" + nullable: true + + EncryptionKey: + type: object + properties: + id: + $ref: "#/components/schemas/ID" + uuid: + $ref: "#/components/schemas/UUID" + type: + $ref: "#/components/schemas/EncryptionKeyType" + encryption_key: + $ref: "#/components/schemas/EncryptionKeyValue" + revoked: + type: boolean + expires: + $ref: "#/components/schemas/EncryptionKeyExpiration" + owner_id: + $ref: "#/components/schemas/ID" + owner_model: + $ref: "#/components/schemas/ModelName" + created: + $ref: "#/components/schemas/DateTime" + modified: + $ref: "#/components/schemas/DateTime" + + EncryptionKeyList: + type: array + items: + $ref: "#/components/schemas/EncryptionKey" + # Errors ApiError: type: object @@ -1210,6 +1309,14 @@ components: schema: $ref: "#/components/schemas/ID" + encryptionKeyId: + name: encryptionKeyId + in: path + description: "Numeric ID of the EncryptionKey" + required: true + schema: + $ref: "#/components/schemas/ID" + quickFilter: name: quickFilter in: query @@ -1659,6 +1766,21 @@ components: type: number format: float + # EncryptionKeys + EncryptionKeyResponse: + description: "Encryption key response" + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKey" + + EncryptionKeyListResponse: + description: "Encryption key list response" + content: + application/json: + schema: + $ref: "#/components/schemas/EncryptionKeyList" + # Errors ApiErrorResponse: description: "Unexpected API error"