• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 19745835717

27 Nov 2025 07:06PM UTC coverage: 40.175%. First build
19745835717

Pull #5837

github

web-flow
Merge 54cd86f6b into 161dabcbd
Pull Request #5837: feat: revoke cert when delete account

47 of 57 new or added lines in 5 files covered. (82.46%)

4825 of 12010 relevant lines covered (40.17%)

3.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

25.45
/lib/Db/CrlMapper.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7
 * SPDX-License-Identifier: AGPL-3.0-or-later
8
 */
9

10
namespace OCA\Libresign\Db;
11

12
use DateTime;
13
use OCA\Libresign\Enum\CRLReason;
14
use OCA\Libresign\Enum\CRLStatus;
15
use OCP\AppFramework\Db\DoesNotExistException;
16
use OCP\AppFramework\Db\QBMapper;
17
use OCP\DB\QueryBuilder\IQueryBuilder;
18
use OCP\IDBConnection;
19

20
/**
21
 * @template-extends QBMapper<Crl>
22
 */
23
class CrlMapper extends QBMapper {
24
        public function __construct(IDBConnection $db) {
25
                parent::__construct($db, 'libresign_crl');
56✔
26
        }
27

28
        public function findBySerialNumber(string $serialNumber): Crl {
29
                $qb = $this->db->getQueryBuilder();
3✔
30

31
                $qb->select('*')
3✔
32
                        ->from($this->getTableName())
3✔
33
                        ->where($qb->expr()->eq('serial_number', $qb->createNamedParameter($serialNumber)));
3✔
34

35
                /** @var Crl */
36
                return $this->findEntity($qb);
3✔
37
        }
38

39
        /**
40
         * Find all issued (non-revoked) certificates owned by a user
41
         *
42
         * @param string $owner User ID
43
         * @return array<Crl>
44
         */
45
        public function findIssuedByOwner(string $owner): array {
NEW
46
                $qb = $this->db->getQueryBuilder();
×
47

NEW
48
                $qb->select('*')
×
NEW
49
                        ->from($this->getTableName())
×
NEW
50
                        ->where($qb->expr()->eq('owner', $qb->createNamedParameter($owner)))
×
NEW
51
                        ->andWhere($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::ISSUED->value)));
×
52

NEW
53
                return $this->findEntities($qb);
×
54
        }
55

56
        public function createCertificate(
57
                string $serialNumber,
58
                string $owner,
59
                string $engine,
60
                string $instanceId,
61
                int $generation,
62
                DateTime $issuedAt,
63
                ?DateTime $validTo = null,
64
        ): Crl {
65
                $certificate = new Crl();
31✔
66
                $certificate->setSerialNumber($serialNumber);
31✔
67
                $certificate->setOwner($owner);
31✔
68
                $certificate->setStatus(CRLStatus::ISSUED);
31✔
69
                $certificate->setIssuedAt($issuedAt);
31✔
70
                $certificate->setValidTo($validTo);
31✔
71
                $certificate->setEngine($engine);
31✔
72
                $certificate->setInstanceId($instanceId);
31✔
73
                $certificate->setGeneration($generation);
31✔
74

75
                /** @var Crl */
76
                return $this->insert($certificate);
31✔
77
        }
78

79
        public function revokeCertificate(
80
                string $serialNumber,
81
                CRLReason $reason = CRLReason::UNSPECIFIED,
82
                ?string $comment = null,
83
                ?string $revokedBy = null,
84
                ?DateTime $invalidityDate = null,
85
                ?int $crlNumber = null,
86
        ): Crl {
87
                $certificate = $this->findBySerialNumber($serialNumber);
×
88

89
                if (CRLStatus::from($certificate->getStatus()) !== CRLStatus::ISSUED) {
×
90
                        throw new \InvalidArgumentException('Certificate is not in issued status');
×
91
                }
92

93
                $certificate->setStatus(CRLStatus::REVOKED);
×
94
                $certificate->setReasonCode($reason->value);
×
95
                $certificate->setComment($comment !== '' ? $comment : null);
×
96
                $certificate->setRevokedBy($revokedBy);
×
97
                $certificate->setRevokedAt(new DateTime());
×
98
                $certificate->setInvalidityDate($invalidityDate);
×
99
                $certificate->setCrlNumber($crlNumber);
×
100

101
                /** @var Crl */
102
                return $this->update($certificate);
×
103
        }
104

105
        public function getRevokedCertificates(string $instanceId = '', int $generation = 0, string $engineType = ''): array {
106
                $qb = $this->db->getQueryBuilder();
6✔
107

108
                $qb->select('*')
6✔
109
                        ->from($this->getTableName())
6✔
110
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
6✔
111
                        ->orderBy('revoked_at', 'DESC');
6✔
112

113
                if ($instanceId !== '') {
6✔
114
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)));
6✔
115
                }
116
                if ($generation !== 0) {
6✔
117
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)));
6✔
118
                }
119
                if ($engineType !== '') {
6✔
120
                        $engineName = match($engineType) {
6✔
121
                                'o' => 'openssl',
6✔
122
                                'c' => 'cfssl',
×
123
                                'openssl', 'cfssl' => $engineType,
×
124
                                default => throw new \InvalidArgumentException("Invalid engine type: $engineType"),
×
125
                        };
6✔
126
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineName)));
6✔
127
                }
128

129
                return $this->findEntities($qb);
6✔
130
        }
131

132
        public function isInvalidAt(string $serialNumber, ?DateTime $checkDate = null): bool {
133
                $checkDate = $checkDate ?? new DateTime();
×
134

135
                try {
136
                        $certificate = $this->findBySerialNumber($serialNumber);
×
137
                } catch (DoesNotExistException $e) {
×
138
                        return false;
×
139
                }
140

141
                if ($certificate->isRevoked()) {
×
142
                        return true;
×
143
                }
144

145
                if ($certificate->getInvalidityDate() && $certificate->getInvalidityDate() <= $checkDate) {
×
146
                        return true;
×
147
                }
148

149
                return false;
×
150
        }
151

152
        public function cleanupExpiredCertificates(?DateTime $before = null): int {
153
                $before = $before ?? new DateTime('-1 year');
×
154

155
                $qb = $this->db->getQueryBuilder();
×
156

157
                return $qb->delete($this->getTableName())
×
158
                        ->where($qb->expr()->isNotNull('valid_to'))
×
159
                        ->andWhere($qb->expr()->lt('valid_to', $qb->createNamedParameter($before, 'datetime')))
×
160
                        ->executeStatement();
×
161
        }
162

163
        public function getStatistics(): array {
164
                $qb = $this->db->getQueryBuilder();
×
165

166
                $result = $qb->select('status', $qb->func()->count('*', 'count'))
×
167
                        ->from($this->getTableName())
×
168
                        ->groupBy('status')
×
169
                        ->executeQuery();
×
170

171
                $stats = [];
×
172
                while ($row = $result->fetch()) {
×
173
                        $stats[$row['status']] = (int)$row['count'];
×
174
                }
175

176
                $result->closeCursor();
×
177
                return $stats;
×
178
        }
179

180
        public function getRevocationStatistics(): array {
181
                $qb = $this->db->getQueryBuilder();
×
182

183
                $result = $qb->select('reason_code', $qb->func()->count('*', 'count'))
×
184
                        ->from($this->getTableName())
×
185
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
×
186
                        ->andWhere($qb->expr()->isNotNull('reason_code'))
×
187
                        ->groupBy('reason_code')
×
188
                        ->executeQuery();
×
189

190
                $stats = [];
×
191
                while ($row = $result->fetch()) {
×
192
                        $reasonCode = (int)$row['reason_code'];
×
193
                        $reason = CRLReason::tryFrom($reasonCode);
×
194
                        $stats[$reasonCode] = [
×
195
                                'code' => $reasonCode,
×
196
                                'description' => $reason?->getDescription() ?? 'unknown',
×
197
                                'count' => (int)$row['count'],
×
198
                        ];
×
199
                }
200

201
                $result->closeCursor();
×
202
                return $stats;
×
203
        }
204

205
        public function getLastCrlNumber(string $instanceId, int $generation, string $engineType): int {
206
                $qb = $this->db->getQueryBuilder();
6✔
207

208
                $qb->select($qb->func()->max('crl_number'))
6✔
209
                        ->from($this->getTableName())
6✔
210
                        ->where($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)))
6✔
211
                        ->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)))
6✔
212
                        ->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineType)))
6✔
213
                        ->andWhere($qb->expr()->isNotNull('crl_number'));
6✔
214

215
                $result = $qb->executeQuery();
6✔
216
                $maxCrlNumber = $result->fetchOne();
6✔
217
                $result->closeCursor();
6✔
218

219
                return (int)($maxCrlNumber ?? 0);
6✔
220
        }
221

222
        /**
223
         * List CRL entries with pagination and filters
224
         *
225
         * @param int $page Page number (1-based)
226
         * @param int $length Number of items per page
227
         * @param array<string, mixed> $filter Filters to apply (status, engine, instance_id, owner, etc.)
228
         * @param array<string, string> $sort Sort fields and directions ['field' => 'ASC|DESC']
229
         * @return array{data: array<Crl>, total: int}
230
         */
231
        public function listWithPagination(
232
                int $page = 1,
233
                int $length = 100,
234
                array $filter = [],
235
                array $sort = [],
236
        ): array {
237
                $qb = $this->db->getQueryBuilder();
×
238

239
                $qb->select('*')
×
240
                        ->from($this->getTableName());
×
241

242
                if (!empty($filter['status'])) {
×
243
                        $qb->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($filter['status'])));
×
244
                }
245

246
                if (!empty($filter['engine'])) {
×
247
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($filter['engine'])));
×
248
                }
249

250
                if (!empty($filter['instance_id'])) {
×
251
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($filter['instance_id'])));
×
252
                }
253

254
                if (!empty($filter['generation'])) {
×
255
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
256
                }
257

258
                if (!empty($filter['owner'])) {
×
259
                        $qb->andWhere($qb->expr()->like('owner', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
260
                }
261

262
                if (!empty($filter['serial_number'])) {
×
263
                        $qb->andWhere($qb->expr()->like('serial_number', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
264
                }
265

266
                if (!empty($filter['revoked_by'])) {
×
267
                        $qb->andWhere($qb->expr()->like('revoked_by', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
268
                }
269

270
                $countQb = $this->db->getQueryBuilder();
×
271
                $countQb->select($countQb->func()->count('*', 'count'))
×
272
                        ->from($this->getTableName());
×
273

274
                if (!empty($filter['status'])) {
×
275
                        $countQb->andWhere($countQb->expr()->eq('status', $countQb->createNamedParameter($filter['status'])));
×
276
                }
277
                if (!empty($filter['engine'])) {
×
278
                        $countQb->andWhere($countQb->expr()->eq('engine', $countQb->createNamedParameter($filter['engine'])));
×
279
                }
280
                if (!empty($filter['instance_id'])) {
×
281
                        $countQb->andWhere($countQb->expr()->eq('instance_id', $countQb->createNamedParameter($filter['instance_id'])));
×
282
                }
283
                if (!empty($filter['generation'])) {
×
284
                        $countQb->andWhere($countQb->expr()->eq('generation', $countQb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
285
                }
286
                if (!empty($filter['owner'])) {
×
287
                        $countQb->andWhere($countQb->expr()->like('owner', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
288
                }
289
                if (!empty($filter['serial_number'])) {
×
290
                        $countQb->andWhere($countQb->expr()->like('serial_number', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
291
                }
292
                if (!empty($filter['revoked_by'])) {
×
293
                        $countQb->andWhere($countQb->expr()->like('revoked_by', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
294
                }
295

296
                $total = (int)$countQb->executeQuery()->fetchOne();
×
297

298
                $allowedSortFields = [
×
299
                        'serial_number',
×
300
                        'owner',
×
301
                        'status',
×
302
                        'engine',
×
303
                        'issued_at',
×
304
                        'valid_to',
×
305
                        'revoked_at',
×
306
                        'reason_code',
×
307
                ];
×
308

309
                if (!empty($sort)) {
×
310
                        foreach ($sort as $field => $direction) {
×
311
                                if (!in_array($field, $allowedSortFields, true)) {
×
312
                                        continue;
×
313
                                }
314
                                $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
×
315
                                $qb->addOrderBy($field, $direction);
×
316
                        }
317
                } else {
318
                        $qb->orderBy('revoked_at', 'DESC')
×
319
                                ->addOrderBy('issued_at', 'DESC');
×
320
                }
321

322
                $offset = ($page - 1) * $length;
×
323
                $qb->setFirstResult($offset)
×
324
                        ->setMaxResults($length);
×
325

326
                return [
×
327
                        'data' => $this->findEntities($qb),
×
328
                        'total' => $total,
×
329
                ];
×
330
        }
331
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc