Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/auth/provider/PhabricatorAuthProvider.php
12256 views
1
<?php
2
3
abstract class PhabricatorAuthProvider extends Phobject {
4
5
private $providerConfig;
6
7
public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
8
$this->providerConfig = $config;
9
return $this;
10
}
11
12
public function hasProviderConfig() {
13
return (bool)$this->providerConfig;
14
}
15
16
public function getProviderConfig() {
17
if ($this->providerConfig === null) {
18
throw new PhutilInvalidStateException('attachProviderConfig');
19
}
20
return $this->providerConfig;
21
}
22
23
public function getProviderConfigPHID() {
24
return $this->getProviderConfig()->getPHID();
25
}
26
27
public function getConfigurationHelp() {
28
return null;
29
}
30
31
public function getDefaultProviderConfig() {
32
return id(new PhabricatorAuthProviderConfig())
33
->setProviderClass(get_class($this))
34
->setIsEnabled(1)
35
->setShouldAllowLogin(1)
36
->setShouldAllowRegistration(1)
37
->setShouldAllowLink(1)
38
->setShouldAllowUnlink(1);
39
}
40
41
public function getNameForCreate() {
42
return $this->getProviderName();
43
}
44
45
public function getDescriptionForCreate() {
46
return null;
47
}
48
49
public function getProviderKey() {
50
return $this->getAdapter()->getAdapterKey();
51
}
52
53
public function getProviderType() {
54
return $this->getAdapter()->getAdapterType();
55
}
56
57
public function getProviderDomain() {
58
return $this->getAdapter()->getAdapterDomain();
59
}
60
61
public static function getAllBaseProviders() {
62
return id(new PhutilClassMapQuery())
63
->setAncestorClass(__CLASS__)
64
->execute();
65
}
66
67
public static function getAllProviders() {
68
static $providers;
69
70
if ($providers === null) {
71
$objects = self::getAllBaseProviders();
72
73
$configs = id(new PhabricatorAuthProviderConfigQuery())
74
->setViewer(PhabricatorUser::getOmnipotentUser())
75
->execute();
76
77
$providers = array();
78
foreach ($configs as $config) {
79
if (!isset($objects[$config->getProviderClass()])) {
80
// This configuration is for a provider which is not installed.
81
continue;
82
}
83
84
$object = clone $objects[$config->getProviderClass()];
85
$object->attachProviderConfig($config);
86
87
$key = $object->getProviderKey();
88
if (isset($providers[$key])) {
89
throw new Exception(
90
pht(
91
"Two authentication providers use the same provider key ".
92
"('%s'). Each provider must be identified by a unique key.",
93
$key));
94
}
95
$providers[$key] = $object;
96
}
97
}
98
99
return $providers;
100
}
101
102
public static function getAllEnabledProviders() {
103
$providers = self::getAllProviders();
104
foreach ($providers as $key => $provider) {
105
if (!$provider->isEnabled()) {
106
unset($providers[$key]);
107
}
108
}
109
return $providers;
110
}
111
112
public static function getEnabledProviderByKey($provider_key) {
113
return idx(self::getAllEnabledProviders(), $provider_key);
114
}
115
116
abstract public function getProviderName();
117
abstract public function getAdapter();
118
119
public function isEnabled() {
120
return $this->getProviderConfig()->getIsEnabled();
121
}
122
123
public function shouldAllowLogin() {
124
return $this->getProviderConfig()->getShouldAllowLogin();
125
}
126
127
public function shouldAllowRegistration() {
128
if (!$this->shouldAllowLogin()) {
129
return false;
130
}
131
132
return $this->getProviderConfig()->getShouldAllowRegistration();
133
}
134
135
public function shouldAllowAccountLink() {
136
return $this->getProviderConfig()->getShouldAllowLink();
137
}
138
139
public function shouldAllowAccountUnlink() {
140
return $this->getProviderConfig()->getShouldAllowUnlink();
141
}
142
143
public function shouldTrustEmails() {
144
return $this->shouldAllowEmailTrustConfiguration() &&
145
$this->getProviderConfig()->getShouldTrustEmails();
146
}
147
148
/**
149
* Should we allow the adapter to be marked as "trusted". This is true for
150
* all adapters except those that allow the user to type in emails (see
151
* @{class:PhabricatorPasswordAuthProvider}).
152
*/
153
public function shouldAllowEmailTrustConfiguration() {
154
return true;
155
}
156
157
public function buildLoginForm(PhabricatorAuthStartController $controller) {
158
return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
159
}
160
161
public function buildInviteForm(PhabricatorAuthStartController $controller) {
162
return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
163
}
164
165
abstract public function processLoginRequest(
166
PhabricatorAuthLoginController $controller);
167
168
public function buildLinkForm($controller) {
169
return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
170
}
171
172
public function shouldAllowAccountRefresh() {
173
return true;
174
}
175
176
public function buildRefreshForm(
177
PhabricatorAuthLinkController $controller) {
178
return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
179
}
180
181
protected function renderLoginForm(AphrontRequest $request, $mode) {
182
throw new PhutilMethodNotImplementedException();
183
}
184
185
public function createProviders() {
186
return array($this);
187
}
188
189
protected function willSaveAccount(PhabricatorExternalAccount $account) {
190
return;
191
}
192
193
final protected function newExternalAccountForIdentifiers(
194
array $identifiers) {
195
196
assert_instances_of($identifiers, 'PhabricatorExternalAccountIdentifier');
197
198
if (!$identifiers) {
199
throw new Exception(
200
pht(
201
'Authentication provider (of class "%s") is attempting to '.
202
'load or create an external account, but provided no account '.
203
'identifiers.',
204
get_class($this)));
205
}
206
207
$config = $this->getProviderConfig();
208
$viewer = PhabricatorUser::getOmnipotentUser();
209
210
$raw_identifiers = mpull($identifiers, 'getIdentifierRaw');
211
212
$accounts = id(new PhabricatorExternalAccountQuery())
213
->setViewer($viewer)
214
->withProviderConfigPHIDs(array($config->getPHID()))
215
->withRawAccountIdentifiers($raw_identifiers)
216
->needAccountIdentifiers(true)
217
->execute();
218
if (!$accounts) {
219
$account = $this->newExternalAccount();
220
} else if (count($accounts) === 1) {
221
$account = head($accounts);
222
} else {
223
throw new Exception(
224
pht(
225
'Authentication provider (of class "%s") is attempting to load '.
226
'or create an external account, but provided a list of '.
227
'account identifiers which map to more than one account: %s.',
228
get_class($this),
229
implode(', ', $raw_identifiers)));
230
}
231
232
// See T13493. Add all the identifiers to the account. In the case where
233
// an account initially has a lower-quality identifier (like an email
234
// address) and later adds a higher-quality identifier (like a GUID), this
235
// allows us to automatically upgrade toward the higher-quality identifier
236
// and survive API changes which remove the lower-quality identifier more
237
// gracefully.
238
239
foreach ($identifiers as $identifier) {
240
$account->appendIdentifier($identifier);
241
}
242
243
return $this->didUpdateAccount($account);
244
}
245
246
final protected function newExternalAccountForUser(PhabricatorUser $user) {
247
$config = $this->getProviderConfig();
248
249
// When a user logs in with a provider like username/password, they
250
// always already have a Phabricator account (since there's no way they
251
// could have a username otherwise).
252
253
// These users should never go to registration, so we're building a
254
// dummy "external account" which just links directly back to their
255
// internal account.
256
257
$account = id(new PhabricatorExternalAccountQuery())
258
->setViewer($user)
259
->withProviderConfigPHIDs(array($config->getPHID()))
260
->withUserPHIDs(array($user->getPHID()))
261
->executeOne();
262
if (!$account) {
263
$account = $this->newExternalAccount()
264
->setUserPHID($user->getPHID());
265
}
266
267
return $this->didUpdateAccount($account);
268
}
269
270
private function didUpdateAccount(PhabricatorExternalAccount $account) {
271
$adapter = $this->getAdapter();
272
273
$account->setUsername($adapter->getAccountName());
274
$account->setRealName($adapter->getAccountRealName());
275
$account->setEmail($adapter->getAccountEmail());
276
$account->setAccountURI($adapter->getAccountURI());
277
278
$account->setProfileImagePHID(null);
279
$image_uri = $adapter->getAccountImageURI();
280
if ($image_uri) {
281
try {
282
$name = PhabricatorSlug::normalize($this->getProviderName());
283
$name = $name.'-profile.jpg';
284
285
// TODO: If the image has not changed, we do not need to make a new
286
// file entry for it, but there's no convenient way to do this with
287
// PhabricatorFile right now. The storage will get shared, so the impact
288
// here is negligible.
289
290
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
291
$image_file = PhabricatorFile::newFromFileDownload(
292
$image_uri,
293
array(
294
'name' => $name,
295
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
296
));
297
if ($image_file->isViewableImage()) {
298
$image_file
299
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
300
->setCanCDN(true)
301
->save();
302
$account->setProfileImagePHID($image_file->getPHID());
303
} else {
304
$image_file->delete();
305
}
306
unset($unguarded);
307
308
} catch (Exception $ex) {
309
// Log this but proceed, it's not especially important that we
310
// be able to pull profile images.
311
phlog($ex);
312
}
313
}
314
315
$this->willSaveAccount($account);
316
317
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
318
$account->save();
319
unset($unguarded);
320
321
return $account;
322
}
323
324
public function getLoginURI() {
325
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
326
return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
327
}
328
329
public function getSettingsURI() {
330
return '/settings/panel/external/';
331
}
332
333
public function getStartURI() {
334
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
335
$uri = $app->getApplicationURI('/start/');
336
return $uri;
337
}
338
339
public function isDefaultRegistrationProvider() {
340
return false;
341
}
342
343
public function shouldRequireRegistrationPassword() {
344
return false;
345
}
346
347
public function newDefaultExternalAccount() {
348
return $this->newExternalAccount();
349
}
350
351
protected function newExternalAccount() {
352
$config = $this->getProviderConfig();
353
$adapter = $this->getAdapter();
354
355
$account = id(new PhabricatorExternalAccount())
356
->setProviderConfigPHID($config->getPHID())
357
->attachAccountIdentifiers(array());
358
359
// TODO: Remove this when these columns are removed. They no longer have
360
// readers or writers (other than this callsite).
361
362
$account
363
->setAccountType($adapter->getAdapterType())
364
->setAccountDomain($adapter->getAdapterDomain());
365
366
// TODO: Remove this when "accountID" is removed; the column is not
367
// nullable.
368
369
$account->setAccountID('');
370
371
return $account;
372
}
373
374
public function getLoginOrder() {
375
return '500-'.$this->getProviderName();
376
}
377
378
protected function getLoginIcon() {
379
return 'Generic';
380
}
381
382
public function newIconView() {
383
return id(new PHUIIconView())
384
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
385
->setSpriteIcon($this->getLoginIcon());
386
}
387
388
public function isLoginFormAButton() {
389
return false;
390
}
391
392
public function renderConfigPropertyTransactionTitle(
393
PhabricatorAuthProviderConfigTransaction $xaction) {
394
395
return null;
396
}
397
398
public function readFormValuesFromProvider() {
399
return array();
400
}
401
402
public function readFormValuesFromRequest(AphrontRequest $request) {
403
return array();
404
}
405
406
public function processEditForm(
407
AphrontRequest $request,
408
array $values) {
409
410
$errors = array();
411
$issues = array();
412
413
return array($errors, $issues, $values);
414
}
415
416
public function extendEditForm(
417
AphrontRequest $request,
418
AphrontFormView $form,
419
array $values,
420
array $issues) {
421
422
return;
423
}
424
425
public function willRenderLinkedAccount(
426
PhabricatorUser $viewer,
427
PHUIObjectItemView $item,
428
PhabricatorExternalAccount $account) {
429
430
$account_view = id(new PhabricatorAuthAccountView())
431
->setExternalAccount($account)
432
->setAuthProvider($this);
433
434
$item->appendChild(
435
phutil_tag(
436
'div',
437
array(
438
'class' => 'mmr mml mst mmb',
439
),
440
$account_view));
441
}
442
443
/**
444
* Return true to use a two-step configuration (setup, configure) instead of
445
* the default single-step configuration. In practice, this means that
446
* creating a new provider instance will redirect back to the edit page
447
* instead of the provider list.
448
*
449
* @return bool True if this provider uses two-step configuration.
450
*/
451
public function hasSetupStep() {
452
return false;
453
}
454
455
/**
456
* Render a standard login/register button element.
457
*
458
* The `$attributes` parameter takes these keys:
459
*
460
* - `uri`: URI the button should take the user to when clicked.
461
* - `method`: Optional HTTP method the button should use, defaults to GET.
462
*
463
* @param AphrontRequest HTTP request.
464
* @param string Request mode string.
465
* @param map Additional parameters, see above.
466
* @return wild Log in button.
467
*/
468
protected function renderStandardLoginButton(
469
AphrontRequest $request,
470
$mode,
471
array $attributes = array()) {
472
473
PhutilTypeSpec::checkMap(
474
$attributes,
475
array(
476
'method' => 'optional string',
477
'uri' => 'string',
478
'sigil' => 'optional string',
479
));
480
481
$viewer = $request->getUser();
482
$adapter = $this->getAdapter();
483
484
if ($mode == 'link') {
485
$button_text = pht('Link External Account');
486
} else if ($mode == 'refresh') {
487
$button_text = pht('Refresh Account Link');
488
} else if ($mode == 'invite') {
489
$button_text = pht('Register Account');
490
} else if ($this->shouldAllowRegistration()) {
491
$button_text = pht('Log In or Register');
492
} else {
493
$button_text = pht('Log In');
494
}
495
496
$icon = id(new PHUIIconView())
497
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
498
->setSpriteIcon($this->getLoginIcon());
499
500
$button = id(new PHUIButtonView())
501
->setSize(PHUIButtonView::BIG)
502
->setColor(PHUIButtonView::GREY)
503
->setIcon($icon)
504
->setText($button_text)
505
->setSubtext($this->getProviderName());
506
507
$uri = $attributes['uri'];
508
$uri = new PhutilURI($uri);
509
$params = $uri->getQueryParamsAsPairList();
510
$uri->removeAllQueryParams();
511
512
$content = array($button);
513
514
foreach ($params as $pair) {
515
list($key, $value) = $pair;
516
$content[] = phutil_tag(
517
'input',
518
array(
519
'type' => 'hidden',
520
'name' => $key,
521
'value' => $value,
522
));
523
}
524
525
$static_response = CelerityAPI::getStaticResourceResponse();
526
$static_response->addContentSecurityPolicyURI('form-action', (string)$uri);
527
528
foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) {
529
$static_response->addContentSecurityPolicyURI('form-action', $csp_uri);
530
}
531
532
return phabricator_form(
533
$viewer,
534
array(
535
'method' => idx($attributes, 'method', 'GET'),
536
'action' => (string)$uri,
537
'sigil' => idx($attributes, 'sigil'),
538
),
539
$content);
540
}
541
542
public function renderConfigurationFooter() {
543
return null;
544
}
545
546
public function getAuthCSRFCode(AphrontRequest $request) {
547
$phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
548
if (!strlen($phcid)) {
549
throw new AphrontMalformedRequestException(
550
pht('Missing Client ID Cookie'),
551
pht(
552
'Your browser did not submit a "%s" cookie with client state '.
553
'information in the request. Check that cookies are enabled. '.
554
'If this problem persists, you may need to clear your cookies.',
555
PhabricatorCookies::COOKIE_CLIENTID),
556
true);
557
}
558
559
return PhabricatorHash::weakDigest($phcid);
560
}
561
562
protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
563
$expect = $this->getAuthCSRFCode($request);
564
565
if (!strlen($actual)) {
566
throw new Exception(
567
pht(
568
'The authentication provider did not return a client state '.
569
'parameter in its response, but one was expected. If this '.
570
'problem persists, you may need to clear your cookies.'));
571
}
572
573
if (!phutil_hashes_are_identical($actual, $expect)) {
574
throw new Exception(
575
pht(
576
'The authentication provider did not return the correct client '.
577
'state parameter in its response. If this problem persists, you may '.
578
'need to clear your cookies.'));
579
}
580
}
581
582
public function supportsAutoLogin() {
583
return false;
584
}
585
586
public function getAutoLoginURI(AphrontRequest $request) {
587
throw new PhutilMethodNotImplementedException();
588
}
589
590
protected function getContentSecurityPolicyFormActions() {
591
return array();
592
}
593
594
}
595
596