@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorAuthSSHKeyEditor
4 extends PhabricatorApplicationTransactionEditor {
5
6 private $isAdministrativeEdit;
7
8 public function setIsAdministrativeEdit($is_administrative_edit) {
9 $this->isAdministrativeEdit = $is_administrative_edit;
10 return $this;
11 }
12
13 public function getIsAdministrativeEdit() {
14 return $this->isAdministrativeEdit;
15 }
16
17 public function getEditorApplicationClass() {
18 return PhabricatorAuthApplication::class;
19 }
20
21 public function getEditorObjectsDescription() {
22 return pht('SSH Keys');
23 }
24
25 public function getTransactionTypes() {
26 $types = parent::getTransactionTypes();
27
28 $types[] = PhabricatorAuthSSHKeyTransaction::TYPE_NAME;
29 $types[] = PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
30 $types[] = PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE;
31
32 return $types;
33 }
34
35 protected function getCustomTransactionOldValue(
36 PhabricatorLiskDAO $object,
37 PhabricatorApplicationTransaction $xaction) {
38
39 switch ($xaction->getTransactionType()) {
40 case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
41 return $object->getName();
42 case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
43 return $object->getEntireKey();
44 case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
45 return !$object->getIsActive();
46 }
47
48 }
49
50 protected function getCustomTransactionNewValue(
51 PhabricatorLiskDAO $object,
52 PhabricatorApplicationTransaction $xaction) {
53
54 switch ($xaction->getTransactionType()) {
55 case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
56 case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
57 return $xaction->getNewValue();
58 case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
59 return (bool)$xaction->getNewValue();
60 }
61 }
62
63 protected function applyCustomInternalTransaction(
64 PhabricatorLiskDAO $object,
65 PhabricatorApplicationTransaction $xaction) {
66
67 $value = $xaction->getNewValue();
68 switch ($xaction->getTransactionType()) {
69 case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
70 $object->setName($value);
71 return;
72 case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
73 $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($value);
74
75 $type = $public_key->getType();
76 $body = $public_key->getBody();
77 $comment = $public_key->getComment();
78
79 $object->setKeyType($type);
80 $object->setKeyBody($body);
81 $object->setKeyComment($comment);
82 return;
83 case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
84 if ($value) {
85 $new = null;
86 } else {
87 $new = 1;
88 }
89
90 $object->setIsActive($new);
91 return;
92 }
93 }
94
95 protected function applyCustomExternalTransaction(
96 PhabricatorLiskDAO $object,
97 PhabricatorApplicationTransaction $xaction) {
98 return;
99 }
100
101 protected function validateTransaction(
102 PhabricatorLiskDAO $object,
103 $type,
104 array $xactions) {
105
106 $errors = parent::validateTransaction($object, $type, $xactions);
107 $viewer = $this->requireActor();
108
109 switch ($type) {
110 case PhabricatorAuthSSHKeyTransaction::TYPE_NAME:
111 $missing = $this->validateIsEmptyTextField(
112 $object->getName(),
113 $xactions);
114
115 if ($missing) {
116 $error = new PhabricatorApplicationTransactionValidationError(
117 $type,
118 pht('Required'),
119 pht('SSH key name is required.'),
120 nonempty(last($xactions), null));
121
122 $error->setIsMissingFieldError(true);
123 $errors[] = $error;
124 }
125 break;
126
127 case PhabricatorAuthSSHKeyTransaction::TYPE_KEY:
128 $missing = $this->validateIsEmptyTextField(
129 $object->getName(),
130 $xactions);
131
132 if ($missing) {
133 $error = new PhabricatorApplicationTransactionValidationError(
134 $type,
135 pht('Required'),
136 pht('SSH key material is required.'),
137 nonempty(last($xactions), null));
138
139 $error->setIsMissingFieldError(true);
140 $errors[] = $error;
141 } else {
142 foreach ($xactions as $xaction) {
143 $new = $xaction->getNewValue();
144
145 try {
146 $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($new);
147 } catch (Exception $ex) {
148 $errors[] = new PhabricatorApplicationTransactionValidationError(
149 $type,
150 pht('Invalid'),
151 $ex->getMessage(),
152 $xaction);
153 continue;
154 }
155
156 // The database does not have a unique key on just the <keyBody>
157 // column because we allow multiple accounts to revoke the same
158 // key, so we can't rely on database constraints to prevent users
159 // from adding keys that are on the revocation list back to their
160 // accounts. Explicitly check for a revoked copy of the key.
161
162 $revoked_keys = id(new PhabricatorAuthSSHKeyQuery())
163 ->setViewer($viewer)
164 ->withObjectPHIDs(array($object->getObjectPHID()))
165 ->withIsActive(0)
166 ->withKeys(array($public_key))
167 ->execute();
168 if ($revoked_keys) {
169 $errors[] = new PhabricatorApplicationTransactionValidationError(
170 $type,
171 pht('Revoked'),
172 pht(
173 'This key has been revoked. Choose or generate a new, '.
174 'unique key.'),
175 $xaction);
176 continue;
177 }
178 }
179 }
180 break;
181
182 case PhabricatorAuthSSHKeyTransaction::TYPE_DEACTIVATE:
183 foreach ($xactions as $xaction) {
184 if (!$xaction->getNewValue()) {
185 $errors[] = new PhabricatorApplicationTransactionValidationError(
186 $type,
187 pht('Invalid'),
188 pht('SSH keys can not be reactivated.'),
189 $xaction);
190 }
191 }
192 break;
193 }
194
195 return $errors;
196 }
197
198 protected function didCatchDuplicateKeyException(
199 PhabricatorLiskDAO $object,
200 array $xactions,
201 Exception $ex) {
202
203 $errors = array();
204 $errors[] = new PhabricatorApplicationTransactionValidationError(
205 PhabricatorAuthSSHKeyTransaction::TYPE_KEY,
206 pht('Duplicate'),
207 pht(
208 'This public key is already associated with another user or device. '.
209 'Each key must unambiguously identify a single unique owner.'),
210 null);
211
212 throw new PhabricatorApplicationTransactionValidationException($errors);
213 }
214
215
216 protected function shouldSendMail(
217 PhabricatorLiskDAO $object,
218 array $xactions) {
219 return true;
220 }
221
222 protected function getMailSubjectPrefix() {
223 return pht('[SSH Key]');
224 }
225
226 protected function getMailThreadID(PhabricatorLiskDAO $object) {
227 return 'ssh-key-'.$object->getPHID();
228 }
229
230 protected function applyFinalEffects(
231 PhabricatorLiskDAO $object,
232 array $xactions) {
233
234 // After making any change to an SSH key, drop the authfile cache so it
235 // is regenerated the next time anyone authenticates.
236 PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
237
238 return $xactions;
239 }
240
241
242 protected function getMailTo(PhabricatorLiskDAO $object) {
243 return $object->getObject()->getSSHKeyNotifyPHIDs();
244 }
245
246 protected function getMailCC(PhabricatorLiskDAO $object) {
247 return array();
248 }
249
250 protected function buildReplyHandler(PhabricatorLiskDAO $object) {
251 return id(new PhabricatorAuthSSHKeyReplyHandler())
252 ->setMailReceiver($object);
253 }
254
255 protected function buildMailTemplate(PhabricatorLiskDAO $object) {
256 $id = $object->getID();
257 $name = $object->getName();
258
259 $mail = id(new PhabricatorMetaMTAMail())
260 ->setSubject(pht('SSH Key %d: %s', $id, $name));
261
262 // The primary value of this mail is alerting users to account compromises,
263 // so force delivery. In particular, this mail should still be delivered
264 // even if "self mail" is disabled.
265 $mail->setForceDelivery(true);
266
267 return $mail;
268 }
269
270 protected function buildMailBody(
271 PhabricatorLiskDAO $object,
272 array $xactions) {
273
274 $body = parent::buildMailBody($object, $xactions);
275
276 if (!$this->getIsAdministrativeEdit()) {
277 $body->addTextSection(
278 pht('SECURITY WARNING'),
279 pht(
280 'If you do not recognize this change, it may indicate your account '.
281 'has been compromised.'));
282 }
283
284 $detail_uri = $object->getURI();
285 $detail_uri = PhabricatorEnv::getProductionURI($detail_uri);
286
287 $body->addLinkSection(pht('SSH KEY DETAIL'), $detail_uri);
288
289 return $body;
290 }
291
292
293 protected function getCustomWorkerState() {
294 return array(
295 'isAdministrativeEdit' => $this->isAdministrativeEdit,
296 );
297 }
298
299 protected function loadCustomWorkerState(array $state) {
300 $this->isAdministrativeEdit = idx($state, 'isAdministrativeEdit');
301 return $this;
302 }
303
304
305}