Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.00% covered (warning)
75.00%
42 / 56
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CrlApiController
75.00% covered (warning)
75.00%
42 / 56
66.67% covered (warning)
66.67%
2 / 3
12.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 list
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 revoke
65.85% covered (warning)
65.85%
27 / 41
0.00% covered (danger)
0.00%
0 / 1
10.55
1<?php
2
3declare(strict_types=1);
4
5/**
6 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7 * SPDX-License-Identifier: AGPL-3.0-or-later
8 */
9
10namespace OCA\Libresign\Controller;
11
12use OCA\Libresign\Enum\CRLReason;
13use OCA\Libresign\Service\Crl\CrlService;
14use OCP\AppFramework\Http;
15use OCP\AppFramework\Http\Attribute\ApiRoute;
16use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
17use OCP\AppFramework\Http\Attribute\OpenAPI;
18use OCP\AppFramework\Http\DataResponse;
19use OCP\IRequest;
20use OCP\IUserSession;
21
22/**
23 * @psalm-import-type LibresignCrlListResponse from \OCA\Libresign\ResponseDefinitions
24 * @psalm-import-type LibresignCrlRevokeResponse from \OCA\Libresign\ResponseDefinitions
25 */
26class CrlApiController extends AEnvironmentAwareController {
27    public function __construct(
28        string $appName,
29        IRequest $request,
30        private CrlService $crlService,
31        private IUserSession $userSession,
32    ) {
33        parent::__construct($appName, $request);
34    }
35
36    /**
37     * List CRL entries with pagination and filters
38     *
39     * @param int|null $page Page number (1-based)
40     * @param int|null $length Number of items per page
41     * @param string|null $status Filter by status (issued, revoked, expired)
42     * @param string|null $engine Filter by engine type
43     * @param string|null $instanceId Filter by instance ID
44     * @param int|null $generation Filter by generation
45     * @param string|null $owner Filter by owner
46     * @param string|null $serialNumber Filter by serial number (partial match)
47     * @param string|null $revokedBy Filter by who revoked the certificate
48     * @param string|null $sortBy Sort field (e.g., 'revoked_at', 'issued_at', 'serial_number')
49     * @param string|null $sortOrder Sort order (ASC or DESC)
50     * @return DataResponse<Http::STATUS_OK, LibresignCrlListResponse, array{}>
51     *
52     * 200: CRL entries retrieved successfully
53     */
54    #[NoCSRFRequired]
55    #[OpenAPI(tags: ['crl'])]
56    #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/crl/list', requirements: ['apiVersion' => '(v1)'])]
57    public function list(
58        ?int $page = null,
59        ?int $length = null,
60        ?string $status = null,
61        ?string $engine = null,
62        ?string $instanceId = null,
63        ?int $generation = null,
64        ?string $owner = null,
65        ?string $serialNumber = null,
66        ?string $revokedBy = null,
67        ?string $sortBy = null,
68        ?string $sortOrder = null,
69    ): DataResponse {
70        $filter = array_filter([
71            'status' => $status,
72            'engine' => $engine,
73            'instance_id' => $instanceId,
74            'generation' => $generation,
75            'owner' => $owner,
76            'serial_number' => $serialNumber,
77            'revoked_by' => $revokedBy,
78        ], fn ($value) => $value !== null);
79
80        $sort = [];
81        if ($sortBy !== null) {
82            $sort[$sortBy] = $sortOrder ?? 'DESC';
83        }
84
85        /** @var LibresignCrlListResponse $result */
86        $result = $this->crlService->listCrlEntries($page, $length, $filter, $sort);
87
88        return new DataResponse($result);
89    }
90
91    /**
92     * Revoke a certificate by serial number
93     *
94     * @param string $serialNumber Certificate serial number to revoke
95     * @param int|null $reasonCode Revocation reason code (0-10, see RFC 5280)
96     * @param string|null $reasonText Optional text describing the reason
97     * @return DataResponse<Http::STATUS_OK, LibresignCrlRevokeResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignCrlRevokeResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignCrlRevokeResponse, array{}>
98     *
99     * 200: Certificate revoked successfully
100     * 400: Invalid parameters
101     * 404: Certificate not found
102     */
103    #[NoCSRFRequired]
104    #[OpenAPI(tags: ['crl'])]
105    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/crl/revoke', requirements: ['apiVersion' => '(v1)'])]
106    public function revoke(
107        string $serialNumber,
108        ?int $reasonCode = null,
109        ?string $reasonText = null,
110    ): DataResponse {
111        if (empty($serialNumber)) {
112            return new DataResponse([
113                'success' => false,
114                'message' => 'Serial number is required',
115            ], Http::STATUS_BAD_REQUEST);
116        }
117
118        // Use default reason code if not provided
119        if ($reasonCode === null) {
120            $reasonCode = 0; // Unspecified
121        }
122
123        $reason = CRLReason::tryFrom($reasonCode);
124        if ($reason === null) {
125            return new DataResponse([
126                'success' => false,
127                'message' => "Invalid reason code: {$reasonCode}. Must be between 0-10 (excluding 7).",
128            ], Http::STATUS_BAD_REQUEST);
129        }
130        assert($reason instanceof CRLReason);
131
132        $user = $this->userSession->getUser();
133        $revokedBy = $user ? $user->getUID() : 'system';
134
135        try {
136            $success = $this->crlService->revokeCertificate(
137                $serialNumber,
138                $reason,
139                $reasonText,
140                $revokedBy
141            );
142
143            if ($success) {
144                return new DataResponse([
145                    'success' => true,
146                    'message' => 'Certificate revoked successfully',
147                ]);
148            } else {
149                return new DataResponse([
150                    'success' => false,
151                    'message' => 'Failed to revoke certificate. It may not exist or already be revoked.',
152                ], Http::STATUS_NOT_FOUND);
153            }
154        } catch (\InvalidArgumentException $e) {
155            return new DataResponse([
156                'success' => false,
157                'message' => $e->getMessage(),
158            ], Http::STATUS_BAD_REQUEST);
159        } catch (\Exception) {
160            return new DataResponse([
161                'success' => false,
162                'message' => 'An error occurred while revoking the certificate',
163            ], Http::STATUS_BAD_REQUEST);
164        }
165    }
166}