@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
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add JIRA as an authentication provider

Summary:
Ref T3687. Depends on D6867. This allows login/registration through JIRA.

The notable difference between this and other providers is that we need to do configuration in two stages, since we need to generate and save a public/private keypair before we can give the user configuration instructions, which takes several seconds and can't change once we've told them to do it.

To this effect, the edit form renders two separate stages, a "setup" stage and a "configure" stage. In the setup stage the user identifies the install and provides the URL. They hit save, we generate a keypair, and take them to the configure stage. In the configure stage, they're walked through setting up all the keys. This ends up feeling a touch rough, but overall pretty reasonable, and we haven't lost much generality.

Test Plan: {F57059}

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T3687

Differential Revision: https://secure.phabricator.com/D6868

+282 -7
+2
src/__phutil_library_map__.php
··· 903 903 'PhabricatorAuthProviderLDAP' => 'applications/auth/provider/PhabricatorAuthProviderLDAP.php', 904 904 'PhabricatorAuthProviderOAuth' => 'applications/auth/provider/PhabricatorAuthProviderOAuth.php', 905 905 'PhabricatorAuthProviderOAuth1' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1.php', 906 + 'PhabricatorAuthProviderOAuth1JIRA' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php', 906 907 'PhabricatorAuthProviderOAuth1Twitter' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php', 907 908 'PhabricatorAuthProviderOAuthAmazon' => 'applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php', 908 909 'PhabricatorAuthProviderOAuthAsana' => 'applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php', ··· 2957 2958 'PhabricatorAuthProviderLDAP' => 'PhabricatorAuthProvider', 2958 2959 'PhabricatorAuthProviderOAuth' => 'PhabricatorAuthProvider', 2959 2960 'PhabricatorAuthProviderOAuth1' => 'PhabricatorAuthProvider', 2961 + 'PhabricatorAuthProviderOAuth1JIRA' => 'PhabricatorAuthProviderOAuth1', 2960 2962 'PhabricatorAuthProviderOAuth1Twitter' => 'PhabricatorAuthProviderOAuth1', 2961 2963 'PhabricatorAuthProviderOAuthAmazon' => 'PhabricatorAuthProviderOAuth', 2962 2964 'PhabricatorAuthProviderOAuthAsana' => 'PhabricatorAuthProviderOAuth',
+15 -4
src/applications/auth/controller/config/PhabricatorAuthEditController.php
··· 97 97 98 98 if (!$errors) { 99 99 if ($is_new) { 100 - $config->setProviderType($provider->getProviderType()); 101 - $config->setProviderDomain($provider->getProviderDomain()); 100 + if (!strlen($config->getProviderType())) { 101 + $config->setProviderType($provider->getProviderType()); 102 + } 103 + if (!strlen($config->getProviderDomain())) { 104 + $config->setProviderDomain($provider->getProviderDomain()); 105 + } 102 106 } 103 107 104 108 $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ··· 134 138 ->setContinueOnNoEffect(true) 135 139 ->applyTransactions($config, $xactions); 136 140 137 - return id(new AphrontRedirectResponse())->setURI( 138 - $this->getApplicationURI()); 141 + 142 + if ($provider->hasSetupStep() && $is_new) { 143 + $id = $config->getID(); 144 + $next_uri = $this->getApplicationURI('config/edit/'.$id.'/'); 145 + } else { 146 + $next_uri = $this->getApplicationURI(); 147 + } 148 + 149 + return id(new AphrontRedirectResponse())->setURI($next_uri); 139 150 } 140 151 } else { 141 152 $properties = $provider->readFormValuesFromProvider();
+13
src/applications/auth/provider/PhabricatorAuthProvider.php
··· 347 347 $account_view)); 348 348 } 349 349 350 + 351 + /** 352 + * Return true to use a two-step configuration (setup, configure) instead of 353 + * the default single-step configuration. In practice, this means that 354 + * creating a new provider instance will redirect back to the edit page 355 + * instead of the provider list. 356 + * 357 + * @return bool True if this provider uses two-step configuration. 358 + */ 359 + public function hasSetupStep() { 360 + return false; 361 + } 362 + 350 363 }
+4 -3
src/applications/auth/provider/PhabricatorAuthProviderOAuth1.php
··· 26 26 protected function configureAdapter(PhutilAuthAdapterOAuth1 $adapter) { 27 27 $config = $this->getProviderConfig(); 28 28 $adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY)); 29 - $adapter->setConsumerSecret( 30 - new PhutilOpaqueEnvelope( 31 - $config->getProperty(self::PROPERTY_CONSUMER_SECRET))); 29 + $secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET); 30 + if (strlen($secret)) { 31 + $adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret)); 32 + } 32 33 $adapter->setCallbackURI($this->getLoginURI()); 33 34 return $adapter; 34 35 }
+248
src/applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthProviderOAuth1JIRA 4 + extends PhabricatorAuthProviderOAuth1 { 5 + 6 + public function getProviderName() { 7 + return pht('JIRA'); 8 + } 9 + 10 + public function getConfigurationHelp() { 11 + if ($this->isSetup()) { 12 + return pht( 13 + "**Step 1 of 2**: Provide the name and URI for your JIRA install.\n\n". 14 + "In the next step, you will configure JIRA."); 15 + } else { 16 + $login_uri = $this->getLoginURI(); 17 + return pht( 18 + "**Step 2 of 2**: In this step, you will configure JIRA.\n\n". 19 + "**Create a JIRA Application**: Log into JIRA and go to ". 20 + "**Administration**, then **Add-ons**, then **Application Links**. ". 21 + "Click the button labeled **Add Application Link**, and use these ". 22 + "settings to create an application:\n\n". 23 + " - **Server URL**: `%s`\n". 24 + " - Then, click **Next**. On the second page:\n". 25 + " - **Application Name**: `Phabricator`\n". 26 + " - **Application Type**: `Generic Application`\n". 27 + " - Then, click **Create**.\n\n". 28 + "**Configure Your Application**: Find the application you just ". 29 + "created in the table, and click the **Configure** link under ". 30 + "**Actions**. Select **Incoming Authentication** and click the ". 31 + "**OAuth** tab (it may be selected by default). Then, use these ". 32 + "settings:\n\n". 33 + " - **Consumer Key**: Set this to the \"Consumer Key\" value in the ". 34 + "form above.\n". 35 + " - **Consumer Name**: `Phabricator`\n". 36 + " - **Public Key**: Set this to the \"Public Key\" value in the ". 37 + "form above.\n". 38 + " - **Consumer Callback URL**: `%s`\n". 39 + "Click **Save** in JIRA. Authentication should now be configured, ". 40 + "and this provider should work correctly.", 41 + PhabricatorEnv::getProductionURI('/'), 42 + $login_uri); 43 + } 44 + } 45 + 46 + protected function newOAuthAdapter() { 47 + $config = $this->getProviderConfig(); 48 + 49 + return id(new PhutilAuthAdapterOAuthJIRA()) 50 + ->setAdapterDomain($config->getProviderDomain()) 51 + ->setJIRABaseURI($config->getProperty(self::PROPERTY_JIRA_URI)) 52 + ->setPrivateKey( 53 + new PhutilOpaqueEnvelope( 54 + $config->getProperty(self::PROPERTY_PRIVATE_KEY))); 55 + } 56 + 57 + protected function getLoginIcon() { 58 + return 'Jira'; 59 + } 60 + 61 + private function isSetup() { 62 + return !$this->getProviderConfig()->getID(); 63 + } 64 + 65 + const PROPERTY_JIRA_NAME = 'oauth1:jira:name'; 66 + const PROPERTY_JIRA_URI = 'oauth1:jira:uri'; 67 + const PROPERTY_PUBLIC_KEY = 'oauth1:jira:key:public'; 68 + const PROPERTY_PRIVATE_KEY = 'oauth1:jira:key:private'; 69 + 70 + 71 + public function readFormValuesFromProvider() { 72 + $config = $this->getProviderConfig(); 73 + $uri = $config->getProperty(self::PROPERTY_JIRA_URI); 74 + 75 + return array( 76 + self::PROPERTY_JIRA_NAME => $this->getProviderDomain(), 77 + self::PROPERTY_JIRA_URI => $uri, 78 + ); 79 + } 80 + 81 + public function readFormValuesFromRequest(AphrontRequest $request) { 82 + $is_setup = $this->isSetup(); 83 + if ($is_setup) { 84 + $name = $request->getStr(self::PROPERTY_JIRA_NAME); 85 + } else { 86 + $name = $this->getProviderDomain(); 87 + } 88 + 89 + return array( 90 + self::PROPERTY_JIRA_NAME => $name, 91 + self::PROPERTY_JIRA_URI => $request->getStr(self::PROPERTY_JIRA_URI), 92 + ); 93 + } 94 + 95 + public function processEditForm( 96 + AphrontRequest $request, 97 + array $values) { 98 + $errors = array(); 99 + $issues = array(); 100 + 101 + $is_setup = $this->isSetup(); 102 + 103 + $key_name = self::PROPERTY_JIRA_NAME; 104 + $key_uri = self::PROPERTY_JIRA_URI; 105 + 106 + if (!strlen($values[$key_name])) { 107 + $errors[] = pht('JIRA instance name is required.'); 108 + $issues[$key_name] = pht('Required'); 109 + } else if (!preg_match('/^[a-z0-9.]+$/', $values[$key_name])) { 110 + $errors[] = pht( 111 + 'JIRA instance name must contain only lowercase letters, digits, and '. 112 + 'period.'); 113 + $issues[$key_name] = pht('Invalid'); 114 + } 115 + 116 + if (!strlen($values[$key_uri])) { 117 + $errors[] = pht('JIRA base URI is required.'); 118 + $issues[$key_uri] = pht('Required'); 119 + } else { 120 + $uri = new PhutilURI($values[$key_uri]); 121 + if (!$uri->getProtocol()) { 122 + $errors[] = pht( 123 + 'JIRA base URI should include protocol (like "https://").'); 124 + $issues[$key_uri] = pht('Invalid'); 125 + } 126 + } 127 + 128 + if (!$errors && $is_setup) { 129 + $config = $this->getProviderConfig(); 130 + 131 + $config->setProviderDomain($values[$key_name]); 132 + 133 + $consumer_key = 'phjira.'.Filesystem::readRandomCharacters(16); 134 + list($public, $private) = PhutilAuthAdapterOAuthJIRA::newJIRAKeypair(); 135 + 136 + $config->setProperty(self::PROPERTY_PUBLIC_KEY, $public); 137 + $config->setProperty(self::PROPERTY_PRIVATE_KEY, $private); 138 + $config->setProperty(self::PROPERTY_CONSUMER_KEY, $consumer_key); 139 + } 140 + 141 + return array($errors, $issues, $values); 142 + } 143 + 144 + public function extendEditForm( 145 + AphrontRequest $request, 146 + AphrontFormView $form, 147 + array $values, 148 + array $issues) { 149 + 150 + if (!function_exists('openssl_pkey_new')) { 151 + // TODO: This could be a bit prettier. 152 + throw new Exception( 153 + pht( 154 + "The PHP 'openssl' extension is not installed. You must install ". 155 + "this extension in order to add a JIRA authentication provider, ". 156 + "because JIRA OAuth requests use the RSA-SHA1 signing algorithm. ". 157 + "Install the 'openssl' extension, restart your webserver, and try ". 158 + "again.")); 159 + } 160 + 161 + $is_setup = $this->isSetup(); 162 + 163 + $e_required = $request->isFormPost() ? null : true; 164 + 165 + $v_name = $values[self::PROPERTY_JIRA_NAME]; 166 + if ($is_setup) { 167 + $e_name = idx($issues, self::PROPERTY_JIRA_NAME, $e_required); 168 + } else { 169 + $e_name = null; 170 + } 171 + 172 + $v_uri = $values[self::PROPERTY_JIRA_URI]; 173 + $e_uri = idx($issues, self::PROPERTY_JIRA_URI, $e_required); 174 + 175 + if ($is_setup) { 176 + $form 177 + ->appendRemarkupInstructions( 178 + pht( 179 + "**JIRA Instance Name**\n\n". 180 + "Choose a permanent name for this instance of JIRA. Phabricator ". 181 + "uses this name internally to keep track of this instance of ". 182 + "JIRA, in case the URL changes later.\n\n". 183 + "Use lowercase letters, digits, and period. For example, ". 184 + "`jira`, `jira.mycompany` or `jira.engineering` are reasonable ". 185 + "names.")) 186 + ->appendChild( 187 + id(new AphrontFormTextControl()) 188 + ->setLabel(pht('JIRA Instance Name')) 189 + ->setValue($v_name) 190 + ->setName(self::PROPERTY_JIRA_NAME) 191 + ->setError($e_name)); 192 + } else { 193 + $form 194 + ->appendChild( 195 + id(new AphrontFormStaticControl()) 196 + ->setLabel(pht('JIRA Instance Name')) 197 + ->setValue($v_name)); 198 + } 199 + 200 + $form 201 + ->appendChild( 202 + id(new AphrontFormTextControl()) 203 + ->setLabel(pht('JIRA Base URI')) 204 + ->setValue($v_uri) 205 + ->setName(self::PROPERTY_JIRA_URI) 206 + ->setCaption( 207 + pht( 208 + 'The URI where JIRA is installed. For example: %s', 209 + phutil_tag('tt', array(), 'https://jira.mycompany.com/'))) 210 + ->setError($e_uri)); 211 + 212 + if (!$is_setup) { 213 + $config = $this->getProviderConfig(); 214 + 215 + 216 + $ckey = $config->getProperty(self::PROPERTY_CONSUMER_KEY); 217 + $ckey = phutil_tag('tt', array(), $ckey); 218 + 219 + $pkey = $config->getProperty(self::PROPERTY_PUBLIC_KEY); 220 + $pkey = phutil_escape_html_newlines($pkey); 221 + $pkey = phutil_tag('tt', array(), $pkey); 222 + 223 + $form 224 + ->appendRemarkupInstructions( 225 + pht( 226 + 'NOTE: **To complete setup**, copy and paste these keys into JIRA '. 227 + 'according to the instructions below.')) 228 + ->appendChild( 229 + id(new AphrontFormStaticControl()) 230 + ->setLabel(pht('Consumer Key')) 231 + ->setValue($ckey)) 232 + ->appendChild( 233 + id(new AphrontFormStaticControl()) 234 + ->setLabel(pht('Public Key')) 235 + ->setValue($pkey)); 236 + } 237 + 238 + } 239 + 240 + 241 + /** 242 + * JIRA uses a setup step to generate public/private keys. 243 + */ 244 + public function hasSetupStep() { 245 + return true; 246 + } 247 + 248 + }