From 3c4070a16822f82d65850379980f42c92729d224 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Wed, 22 Feb 2012 10:21:39 -0800 Subject: [PATCH] OAuth Server -- add controllers to RUD client authorizations and CRUD clients Summary: beyond the title, this diff tweaks the test console to have a bit more functionality. also makes a small change to CSS for AphrontFormControlMarkup, which IMO fixes a display issue on https://secure.phabricator.com/settings/page/profile/ where the Profile URI is all up in the air and whatnot I think this is missing pagination. I am getting tired of the size though and will add later. See T905. Test Plan: viewed, updated and deleted client authorizations. viewed, created, updated and deleted clients Reviewers: epriestley Reviewed By: epriestley CC: aran, epriestley Maniphest Tasks: T849, T850, T848 Differential Revision: https://secure.phabricator.com/D1683 --- .../sql/patches/109.oauthclientphidkey.sql | 3 + src/__phutil_library_map__.php | 22 +- ...AphrontDefaultApplicationConfiguration.php | 21 +- .../response/403/Aphront403Response.php | 17 +- .../PhabricatorOAuthServerAuthController.php | 19 +- .../base/PhabricatorOAuthServerController.php | 84 ++++++++ .../oauthserver/controller/base/__init__.php | 18 ++ .../PhabricatorOAuthClientBaseController.php | 41 ++++ .../controller/client/base/__init__.php | 14 ++ ...PhabricatorOAuthClientDeleteController.php | 65 ++++++ .../controller/client/delete/__init__.php | 21 ++ .../PhabricatorOAuthClientEditController.php | 197 ++++++++++++++++++ .../controller/client/edit/__init__.php | 29 +++ .../PhabricatorOAuthClientListController.php | 140 +++++++++++++ .../controller/client/list/__init__.php | 19 ++ .../PhabricatorOAuthClientViewController.php | 134 ++++++++++++ .../controller/client/view/__init__.php | 21 ++ ...OAuthClientAuthorizationBaseController.php | 41 ++++ .../clientauthorization/base/__init__.php | 14 ++ ...uthClientAuthorizationDeleteController.php | 75 +++++++ .../clientauthorization/delete/__init__.php | 22 ++ ...OAuthClientAuthorizationEditController.php | 114 ++++++++++ .../clientauthorization/edit/__init__.php | 27 +++ ...OAuthClientAuthorizationListController.php | 161 ++++++++++++++ .../clientauthorization/list/__init__.php | 21 ++ .../PhabricatorOAuthServerTestController.php | 85 +++----- .../oauthserver/controller/test/__init__.php | 10 +- .../scope/PhabricatorOAuthServerScope.php | 44 ++++ .../oauthserver/scope/__init__.php | 2 + .../server/PhabricatorOAuthServer.php | 2 + .../client/PhabricatorOAuthServerClient.php | 12 ++ .../PhabricatorOAuthClientAuthorization.php | 15 ++ src/view/layout/panel/AphrontPanelView.php | 7 +- webroot/rsrc/css/aphront/form-view.css | 3 +- webroot/rsrc/css/aphront/panel-view.css | 8 +- 35 files changed, 1450 insertions(+), 78 deletions(-) create mode 100644 resources/sql/patches/109.oauthclientphidkey.sql create mode 100644 src/applications/oauthserver/controller/base/PhabricatorOAuthServerController.php create mode 100644 src/applications/oauthserver/controller/base/__init__.php create mode 100644 src/applications/oauthserver/controller/client/base/PhabricatorOAuthClientBaseController.php create mode 100644 src/applications/oauthserver/controller/client/base/__init__.php create mode 100644 src/applications/oauthserver/controller/client/delete/PhabricatorOAuthClientDeleteController.php create mode 100644 src/applications/oauthserver/controller/client/delete/__init__.php create mode 100644 src/applications/oauthserver/controller/client/edit/PhabricatorOAuthClientEditController.php create mode 100644 src/applications/oauthserver/controller/client/edit/__init__.php create mode 100644 src/applications/oauthserver/controller/client/list/PhabricatorOAuthClientListController.php create mode 100644 src/applications/oauthserver/controller/client/list/__init__.php create mode 100644 src/applications/oauthserver/controller/client/view/PhabricatorOAuthClientViewController.php create mode 100644 src/applications/oauthserver/controller/client/view/__init__.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/base/PhabricatorOAuthClientAuthorizationBaseController.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/base/__init__.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/delete/PhabricatorOAuthClientAuthorizationDeleteController.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/delete/__init__.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/edit/PhabricatorOAuthClientAuthorizationEditController.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/edit/__init__.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/list/PhabricatorOAuthClientAuthorizationListController.php create mode 100644 src/applications/oauthserver/controller/clientauthorization/list/__init__.php diff --git a/resources/sql/patches/109.oauthclientphidkey.sql b/resources/sql/patches/109.oauthclientphidkey.sql new file mode 100644 index 0000000000..f3c84dda78 --- /dev/null +++ b/resources/sql/patches/109.oauthclientphidkey.sql @@ -0,0 +1,3 @@ +ALTER TABLE `phabricator_oauth_server`.`oauth_server_oauthserverclient` + ADD KEY `creatorPHID` (`creatorPHID`) + diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 08be8c8886..aecdbf9191 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -609,6 +609,15 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/mysql', 'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/clientauthorization', + 'PhabricatorOAuthClientAuthorizationBaseController' => 'applications/oauthserver/controller/clientauthorization/base', + 'PhabricatorOAuthClientAuthorizationDeleteController' => 'applications/oauthserver/controller/clientauthorization/delete', + 'PhabricatorOAuthClientAuthorizationEditController' => 'applications/oauthserver/controller/clientauthorization/edit', + 'PhabricatorOAuthClientAuthorizationListController' => 'applications/oauthserver/controller/clientauthorization/list', + 'PhabricatorOAuthClientBaseController' => 'applications/oauthserver/controller/client/base', + 'PhabricatorOAuthClientDeleteController' => 'applications/oauthserver/controller/client/delete', + 'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/edit', + 'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/list', + 'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/view', 'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/default', 'PhabricatorOAuthDiagnosticsController' => 'applications/auth/controller/oauthdiagnostics', 'PhabricatorOAuthFailureView' => 'applications/auth/view/oauthfailure', @@ -625,6 +634,7 @@ phutil_register_library_map(array( 'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/auth', 'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/authorizationcode', 'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/client', + 'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/base', 'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/base', 'PhabricatorOAuthServerScope' => 'applications/oauthserver/scope', 'PhabricatorOAuthServerTestController' => 'applications/oauthserver/controller/test', @@ -1360,6 +1370,15 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorOAuthClientAuthorization' => 'PhabricatorOAuthServerDAO', + 'PhabricatorOAuthClientAuthorizationBaseController' => 'PhabricatorOAuthServerController', + 'PhabricatorOAuthClientAuthorizationDeleteController' => 'PhabricatorOAuthClientAuthorizationBaseController', + 'PhabricatorOAuthClientAuthorizationEditController' => 'PhabricatorOAuthClientAuthorizationBaseController', + 'PhabricatorOAuthClientAuthorizationListController' => 'PhabricatorOAuthClientAuthorizationBaseController', + 'PhabricatorOAuthClientBaseController' => 'PhabricatorOAuthServerController', + 'PhabricatorOAuthClientDeleteController' => 'PhabricatorOAuthClientBaseController', + 'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientBaseController', + 'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientBaseController', + 'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientBaseController', 'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController', 'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController', 'PhabricatorOAuthFailureView' => 'AphrontView', @@ -1374,8 +1393,9 @@ phutil_register_library_map(array( 'PhabricatorOAuthServerAuthController' => 'PhabricatorAuthController', 'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerClient' => 'PhabricatorOAuthServerDAO', + 'PhabricatorOAuthServerController' => 'PhabricatorController', 'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO', - 'PhabricatorOAuthServerTestController' => 'PhabricatorAuthController', + 'PhabricatorOAuthServerTestController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController', 'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorObjectGraph' => 'AbstractDirectedGraph', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index e17f394a4d..65bc917eb7 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -158,9 +158,24 @@ class AphrontDefaultApplicationConfiguration ), '/oauthserver/' => array( - 'auth/' => 'PhabricatorOAuthServerAuthController', - 'token/' => 'PhabricatorOAuthServerTokenController', - 'test/' => 'PhabricatorOAuthServerTestController', + 'auth/' => 'PhabricatorOAuthServerAuthController', + 'test/' => 'PhabricatorOAuthServerTestController', + 'token/' => 'PhabricatorOAuthServerTokenController', + 'clientauthorization/' => array( + '$' => 'PhabricatorOAuthClientAuthorizationListController', + 'delete/(?P[^/]+)/' => + 'PhabricatorOAuthClientAuthorizationDeleteController', + 'edit/(?P[^/]+)/' => + 'PhabricatorOAuthClientAuthorizationEditController', + ), + 'client/' => array( + '$' => 'PhabricatorOAuthClientListController', + 'create/$' => 'PhabricatorOAuthClientEditController', + 'delete/(?P[^/]+)/$' => + 'PhabricatorOAuthClientDeleteController', + 'edit/(?P[^/]+)/$' => 'PhabricatorOAuthClientEditController', + 'view/(?P[^/]+)/$' => 'PhabricatorOAuthClientViewController', + ), ), '/xhprof/' => array( diff --git a/src/aphront/response/403/Aphront403Response.php b/src/aphront/response/403/Aphront403Response.php index 7227b22bfb..74829baced 100644 --- a/src/aphront/response/403/Aphront403Response.php +++ b/src/aphront/response/403/Aphront403Response.php @@ -21,15 +21,28 @@ */ class Aphront403Response extends AphrontWebpageResponse { + private $forbiddenText; + public function setForbiddenText($text) { + $this->forbiddenText = $text; + return $this; + } + private function getForbiddenText() { + return $this->forbiddenText; + } + public function getHTTPResponseCode() { return 403; } public function buildResponseString() { + $forbidden_text = $this->getForbiddenText(); + if (!$forbidden_text) { + $forbidden_text = + 'You do not have privileges to access the requested page.'; + } $failure = new AphrontRequestFailureView(); $failure->setHeader('403 Forbidden'); - $failure->appendChild( - '

You do not have privileges to access the requested page.

'); + $failure->appendChild('

'.$forbidden_text.'

'); $view = new PhabricatorStandardPageView(); $view->setTitle('403 Forbidden'); diff --git a/src/applications/oauthserver/controller/auth/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/auth/PhabricatorOAuthServerAuthController.php index 177defb801..836bfa9d13 100644 --- a/src/applications/oauthserver/controller/auth/PhabricatorOAuthServerAuthController.php +++ b/src/applications/oauthserver/controller/auth/PhabricatorOAuthServerAuthController.php @@ -54,9 +54,7 @@ extends PhabricatorAuthController { $return_auth_code = true; $unguarded_write = AphrontWriteGuard::beginScopedUnguardedWrites(); } else if ($request->isFormPost()) { - // TODO -- T848 (add scope to Phabricator OAuth) - // should have some $scope based off of user submission here...! - $scope = array(PhabricatorOAuthServerScope::SCOPE_WHOAMI => 1); + $scope = PhabricatorOAuthServerScope::getScopesFromRequest($request); $server->authorizeClient($scope); $return_auth_code = true; $unguarded_write = null; @@ -107,21 +105,28 @@ extends PhabricatorAuthController { $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->setHeader($title); - // TODO -- T848 (add scope to Phabricator OAuth) - // generally inform user what this means as this fleshes out $description = "Do want to authorize {$name} to access your ". "Phabricator account data?"; + $desired_scopes = array( + PhabricatorOAuthServerScope::SCOPE_WHOAMI => 1, + PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS => 1 + ); $form = id(new AphrontFormView()) ->setUser($current_user) ->appendChild( id(new AphrontFormStaticControl()) - ->setValue($description)) + ->setValue($description) + ) + ->appendChild( + PhabricatorOAuthServerScope::getCheckboxControl() + ) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Authorize') - ->addCancelButton('/')); + ->addCancelButton('/') + ); // TODO -- T889 (make "cancel" do something more sensible) $panel->appendChild($form); diff --git a/src/applications/oauthserver/controller/base/PhabricatorOAuthServerController.php b/src/applications/oauthserver/controller/base/PhabricatorOAuthServerController.php new file mode 100644 index 0000000000..2a6843e9b3 --- /dev/null +++ b/src/applications/oauthserver/controller/base/PhabricatorOAuthServerController.php @@ -0,0 +1,84 @@ +getRequest()->getUser(); + $page = $this->buildStandardPageView(); + $page->setApplicationName('OAuth Server'); + $page->setBaseURI('/oauthserver/'); + $page->setTitle(idx($data, 'title')); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI('/oauthserver/')); + $nav->addLabel('Client Authorizations'); + $nav->addFilter('clientauthorization', + 'My Authorizations'); + $nav->addSpacer(); + $nav->addLabel('Clients'); + $nav->addFilter('client/create', + 'Create Client'); + foreach ($this->getExtraClientFilters() as $filter) { + $nav->addFilter($filter['url'], + $filter['label']); + } + $nav->addFilter('client', + 'My Clients'); + $nav->selectFilter($this->getFilter(), + 'clientauthorization'); + + $nav->appendChild($view); + + $page->appendChild($nav); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + + protected function getFilter() { + return 'clientauthorization'; + } + + protected function getExtraClientFilters() { + return array(); + } + + protected function getHighlightPHIDs() { + $phids = array(); + $request = $this->getRequest(); + $edited = $request->getStr('edited'); + $new = $request->getStr('new'); + if ($edited) { + $phids[$edited] = $edited; + } + if ($new) { + $phids[$new] = $new; + } + return $phids; + } + + protected function buildErrorView($error_message) { + $error = new AphrontErrorView(); + $error->setSeverity(AphrontErrorView::SEVERITY_ERROR); + $error->setTitle($error_message); + + return $error; + } +} diff --git a/src/applications/oauthserver/controller/base/__init__.php b/src/applications/oauthserver/controller/base/__init__.php new file mode 100644 index 0000000000..7e68dc4780 --- /dev/null +++ b/src/applications/oauthserver/controller/base/__init__.php @@ -0,0 +1,18 @@ +clientPHID; + } + private function setClientPHID($phid) { + $this->clientPHID = $phid; + return $this; + } + + public function shouldRequireLogin() { + return true; + } + + public function willProcessRequest(array $data) { + $this->setClientPHID(idx($data, 'phid')); + } +} diff --git a/src/applications/oauthserver/controller/client/base/__init__.php b/src/applications/oauthserver/controller/client/base/__init__.php new file mode 100644 index 0000000000..e24599e6c5 --- /dev/null +++ b/src/applications/oauthserver/controller/client/base/__init__.php @@ -0,0 +1,14 @@ +getClientPHID(); + $title = 'Delete OAuth Client'; + $request = $this->getRequest(); + $current_user = $request->getUser(); + $client = id(new PhabricatorOAuthServerClient()) + ->loadOneWhere('phid = %s', + $phid); + + if (empty($client)) { + return new Aphront404Response(); + } + if ($client->getCreatorPHID() != $current_user->getPHID()) { + $message = 'Access denied to client with phid '.$phid.'. '. + 'Only the user who created the client has permission to '. + 'delete the client.'; + return id(new Aphront403Response()) + ->setForbiddenText($message); + } + + if ($request->isFormPost()) { + $client->delete(); + return id(new AphrontRedirectResponse()) + ->setURI('/oauthserver/client/?deleted=1'); + } + + $client_name = phutil_escape_html($client->getName()); + $title .= ' '.$client_name; + + $dialog = new AphrontDialogView(); + $dialog->setUser($current_user); + $dialog->setTitle($title); + $dialog->appendChild( + '

Are you sure you want to delete this client?

' + ); + $dialog->addSubmitButton(); + $dialog->addCancelButton($client->getEditURI()); + return id(new AphrontDialogResponse())->setDialog($dialog); + + } +} diff --git a/src/applications/oauthserver/controller/client/delete/__init__.php b/src/applications/oauthserver/controller/client/delete/__init__.php new file mode 100644 index 0000000000..716616cf90 --- /dev/null +++ b/src/applications/oauthserver/controller/client/delete/__init__.php @@ -0,0 +1,21 @@ +isEdit; + } + private function setIsClientEdit($is_edit) { + $this->isEdit = (bool) $is_edit; + return $this; + } + + protected function getExtraClientFilters() { + if ($this->isClientEdit()) { + $filters = array( + array('url' => $this->getFilter(), + 'label' => 'Edit Client') + ); + } else { + $filters = array(); + } + return $filters; + } + + public function getFilter() { + if ($this->isClientEdit()) { + $filter = 'client/edit/'.$this->getClientPHID(); + } else { + $filter = 'client/create'; + } + return $filter; + } + + public function processRequest() { + $request = $this->getRequest(); + $current_user = $request->getUser(); + $error = null; + $bad_redirect = false; + $phid = $this->getClientPHID(); + // if we have a phid, then we're editing + $this->setIsClientEdit($phid); + + if ($this->isClientEdit()) { + $client = id(new PhabricatorOAuthServerClient()) + ->loadOneWhere('phid = %s', + $phid); + $title = 'Edit OAuth Client'; + // validate the client + if (empty($client)) { + return new Aphront404Response(); + } + if ($client->getCreatorPHID() != $current_user->getPHID()) { + $message = 'Access denied to edit client with id '.$phid.'. '. + 'Only the user who created the client has permission to '. + 'edit the client.'; + return id(new Aphront403Response()) + ->setForbiddenText($message); + } + $submit_button = 'Save OAuth Client'; + // new client - much simpler + } else { + $client = new PhabricatorOAuthServerClient(); + $title = 'Create OAuth Client'; + $submit_button = 'Create OAuth Client'; + } + + if ($request->isFormPost()) { + $redirect_uri = $request->getStr('redirect_uri'); + $client->setName($request->getStr('name')); + $client->setRedirectURI($redirect_uri); + $client->setSecret(Filesystem::readRandomCharacters(32)); + $client->setCreatorPHID($current_user->getPHID()); + $uri = new PhutilURI($redirect_uri); + if (!$this->validateRedirectURI($uri)) { + $error = new AphrontErrorView(); + $error->setSeverity(AphrontErrorView::SEVERITY_ERROR); + $error->setTitle( + 'Redirect URI must be a fully qualified domain name.' + ); + $bad_redirect = true; + } else { + $client->save(); + if ($this->isClientEdit()) { + return id(new AphrontRedirectResponse()) + ->setURI('/oauthserver/client/?edited='.$phid); + } else { + return id(new AphrontRedirectResponse()) + ->setURI('/oauthserver/client/?new='.$phid); + } + } + } + + $panel = new AphrontPanelView(); + if ($this->isClientEdit()) { + $delete_button = phutil_render_tag( + 'a', + array( + 'href' => $client->getDeleteURI(), + 'class' => 'grey button', + ), + 'Delete OAuth Client'); + $panel->addButton($delete_button); + } + $panel->setHeader($title); + + $form = id(new AphrontFormView()) + ->setUser($current_user) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setValue($client->getName()) + ); + if ($this->isClientEdit()) { + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('ID') + ->setValue($phid) + ) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Secret') + ->setValue($client->getSecret()) + ); + } + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Redirect URI') + ->setName('redirect_uri') + ->setValue($client->getRedirectURI()) + ->setError($bad_redirect) + ); + if ($this->isClientEdit()) { + $created = phabricator_datetime($client->getDateCreated(), + $current_user); + $updated = phabricator_datetime($client->getDateModified(), + $current_user); + $form + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Created') + ->setValue($created) + ) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Last Updated') + ->setValue($updated) + ); + } + $form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue($submit_button) + ); + + $panel->appendChild($form); + return $this->buildStandardPageResponse( + array($error, + $panel + ), + array('title' => $title) + ); + } + + private function validateRedirectURI(PhutilURI $uri) { + if (PhabricatorEnv::isValidRemoteWebResource($uri)) { + if ($uri->getDomain()) { + return true; + } + } + return false; + } + +} diff --git a/src/applications/oauthserver/controller/client/edit/__init__.php b/src/applications/oauthserver/controller/client/edit/__init__.php new file mode 100644 index 0000000000..d7ec2ce1bb --- /dev/null +++ b/src/applications/oauthserver/controller/client/edit/__init__.php @@ -0,0 +1,29 @@ +getRequest(); + $current_user = $request->getUser(); + $clients = id(new PhabricatorOAuthServerClient()) + ->loadAllWhere('creatorPHID = %s', + $current_user->getPHID()); + + $rows = array(); + $rowc = array(); + $highlight = $this->getHighlightPHIDs(); + foreach ($clients as $client) { + $row = array( + phutil_render_tag( + 'a', + array( + 'href' => $client->getViewURI(), + ), + phutil_escape_html($client->getName()) + ), + $client->getPHID(), + phutil_render_tag( + 'a', + array( + 'href' => $client->getRedirectURI(), + ), + phutil_escape_html($client->getRedirectURI()) + ), + phutil_render_tag( + 'a', + array( + 'class' => 'small button grey', + 'href' => $client->getEditURI(), + ), + 'Edit' + ), + ); + + $rows[] = $row; + if (isset($highlight[$client->getPHID()])) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = ''; + } + } + + $panel = $this->buildClientList($rows, $rowc, $title); + + return $this->buildStandardPageResponse( + array($this->getNoticeView(), + $panel), + array('title' => $title) + ); + } + + private function buildClientList($rows, $rowc, $title) { + $table = new AphrontTableView($rows); + $table->setRowClasses($rowc); + $table->setHeaders( + array( + 'Client', + 'ID', + 'Redirect URI', + '', + )); + $table->setColumnClasses( + array( + '', + '', + '', + 'action', + )); + if (empty($rows)) { + $table->setNoDataString( + 'You have not created any clients for this OAuthServer.' + ); + } + + $panel = new AphrontPanelView(); + $panel->appendChild($table); + $panel->setHeader($title); + + return $panel; + } + + private function getNoticeView() { + $edited = $this->getRequest()->getStr('edited'); + $new = $this->getRequest()->getStr('new'); + $deleted = $this->getRequest()->getBool('deleted'); + if ($edited) { + $edited = phutil_escape_html($edited); + $title = 'Successfully edited client with id '.$edited.'.'; + } else if ($new) { + $new = phutil_escape_html($new); + $title = 'Successfully created client with id '.$new.'.'; + } else if ($deleted) { + $title = 'Successfully deleted client.'; + } else { + $title = null; + } + + if ($title) { + $view = new AphrontErrorView(); + $view->setTitle($title); + $view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); + } else { + $view = null; + } + + return $view; + } +} diff --git a/src/applications/oauthserver/controller/client/list/__init__.php b/src/applications/oauthserver/controller/client/list/__init__.php new file mode 100644 index 0000000000..f33786da85 --- /dev/null +++ b/src/applications/oauthserver/controller/client/list/__init__.php @@ -0,0 +1,19 @@ +getClientPHID(); + } + + protected function getExtraClientFilters() { + return array( + array('url' => $this->getFilter(), + 'label' => 'View Client') + ); + } + + public function processRequest() { + $request = $this->getRequest(); + $current_user = $request->getUser(); + $error = null; + $phid = $this->getClientPHID(); + + $client = id(new PhabricatorOAuthServerClient()) + ->loadOneWhere('phid = %s', + $phid); + $title = 'View OAuth Client'; + + // validate the client + if (empty($client)) { + $message = 'No client found with id '.$phid.'.'; + return $this->buildStandardPageResponse( + $this->buildErrorView($message), + array('title' => $title) + ); + } + + $panel = new AphrontPanelView(); + $panel->setHeader($title); + + $form = id(new AphrontFormView()) + ->setUser($current_user) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Name') + ->setValue($client->getName()) + ) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('ID') + ->setValue($phid) + ); + if ($current_user->getPHID() == $client->getCreatorPHID()) { + $form + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Secret') + ->setValue($client->getSecret()) + ); + } + $form + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Redirect URI') + ->setValue($client->getRedirectURI()) + ); + $created = phabricator_datetime($client->getDateCreated(), + $current_user); + $updated = phabricator_datetime($client->getDateModified(), + $current_user); + $form + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Created') + ->setValue($created) + ) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Last Updated') + ->setValue($updated) + ); + $panel->appendChild($form); + $admin_panel = null; + if ($client->getCreatorPHID() == $current_user->getPHID()) { + $edit_button = phutil_render_tag( + 'a', + array( + 'href' => $client->getEditURI(), + 'class' => 'grey button', + ), + 'Edit OAuth Client'); + $panel->addButton($edit_button); + + $create_authorization_form = id(new AphrontFormView()) + ->setUser($current_user) + ->addHiddenInput('action', 'testclientauthorization') + ->addHiddenInput('client_phid', $phid) + ->setAction('/oauthserver/test/') + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Create Scopeless Test Authorization') + ); + $admin_panel = id(new AphrontPanelView()) + ->setHeader('Admin Tools') + ->appendChild($create_authorization_form); + } + + return $this->buildStandardPageResponse( + array($error, + $panel, + $admin_panel + ), + array('title' => $title) + ); + } +} diff --git a/src/applications/oauthserver/controller/client/view/__init__.php b/src/applications/oauthserver/controller/client/view/__init__.php new file mode 100644 index 0000000000..b139e72150 --- /dev/null +++ b/src/applications/oauthserver/controller/client/view/__init__.php @@ -0,0 +1,21 @@ +authorizationPHID; + } + private function setAuthorizationPHID($phid) { + $this->authorizationPHID = $phid; + return $this; + } + + public function shouldRequireLogin() { + return true; + } + + public function willProcessRequest(array $data) { + $this->setAuthorizationPHID(idx($data, 'phid')); + } +} diff --git a/src/applications/oauthserver/controller/clientauthorization/base/__init__.php b/src/applications/oauthserver/controller/clientauthorization/base/__init__.php new file mode 100644 index 0000000000..3f73c58741 --- /dev/null +++ b/src/applications/oauthserver/controller/clientauthorization/base/__init__.php @@ -0,0 +1,14 @@ +getAuthorizationPHID(); + $title = 'Delete OAuth Client Authorization'; + $request = $this->getRequest(); + $current_user = $request->getUser(); + $authorization = id(new PhabricatorOAuthClientAuthorization()) + ->loadOneWhere('phid = %s', + $phid); + + if (empty($authorization)) { + return new Aphront404Response(); + } + if ($authorization->getUserPHID() != $current_user->getPHID()) { + $message = 'Access denied to client authorization with phid '.$phid.'. '. + 'Only the user who authorized the client has permission to '. + 'delete the authorization.'; + return id(new Aphront403Response()) + ->setForbiddenText($message); + } + + if ($request->isFormPost()) { + $authorization->delete(); + return id(new AphrontRedirectResponse()) + ->setURI('/oauthserver/clientauthorization/?notice=deleted'); + } + + $client_phid = $authorization->getClientPHID(); + $client = id(new PhabricatorOAuthServerClient()) + ->loadOneWhere('phid = %s', + $client_phid); + if ($client) { + $client_name = phutil_escape_html($client->getName()); + $title .= ' for '.$client_name; + } else { + // the client does not exist so token is dead already (but + // let's let the user clean this up anyway in that case) + $client_name = ''; + } + + $dialog = new AphrontDialogView(); + $dialog->setUser($current_user); + $dialog->setTitle($title); + $dialog->appendChild( + '

Are you sure you want to delete this client authorization?

' + ); + $dialog->addSubmitButton(); + $dialog->addCancelButton($authorization->getEditURI()); + return id(new AphrontDialogResponse())->setDialog($dialog); + + } +} diff --git a/src/applications/oauthserver/controller/clientauthorization/delete/__init__.php b/src/applications/oauthserver/controller/clientauthorization/delete/__init__.php new file mode 100644 index 0000000000..ae002c9ac0 --- /dev/null +++ b/src/applications/oauthserver/controller/clientauthorization/delete/__init__.php @@ -0,0 +1,22 @@ +getAuthorizationPHID(); + $title = 'Edit OAuth Client Authorization'; + $request = $this->getRequest(); + $current_user = $request->getUser(); + $authorization = id(new PhabricatorOAuthClientAuthorization()) + ->loadOneWhere('phid = %s', + $phid); + + if (empty($authorization)) { + return new Aphront404Response(); + } + if ($authorization->getUserPHID() != $current_user->getPHID()) { + $message = 'Access denied to client authorization with phid '.$phid.'. '. + 'Only the user who authorized the client has permission to '. + 'edit the authorization.'; + return id(new Aphront403Response()) + ->setForbiddenText($message); + } + + if ($request->isFormPost()) { + $scopes = PhabricatorOAuthServerScope::getScopesFromRequest($request); + $authorization->setScope($scopes); + $authorization->save(); + return id(new AphrontRedirectResponse()) + ->setURI('/oauthserver/clientauthorization/?edited='.$phid); + } + + $client_phid = $authorization->getClientPHID(); + $client = id(new PhabricatorOAuthServerClient()) + ->loadOneWhere('phid = %s', + $client_phid); + + $created = phabricator_datetime($authorization->getDateCreated(), + $current_user); + $updated = phabricator_datetime($authorization->getDateModified(), + $current_user); + + $panel = new AphrontPanelView(); + $delete_button = phutil_render_tag( + 'a', + array( + 'href' => $authorization->getDeleteURI(), + 'class' => 'grey button', + ), + 'Delete OAuth Client Authorization'); + $panel->addButton($delete_button); + $panel->setHeader($title); + + $form = id(new AphrontFormView()) + ->setUser($current_user) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel('Client') + ->setValue( + phutil_render_tag( + 'a', + array( + 'href' => $client->getViewURI(), + ), + phutil_escape_html($client->getName()))) + ) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Created') + ->setValue($created) + ) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Last Updated') + ->setValue($updated) + ) + ->appendChild( + PhabricatorOAuthServerScope::getCheckboxControl( + $authorization->getScope() + ) + ) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Save OAuth Client Authorization') + ->addCancelButton('/oauthserver/clientauthorization/') + ); + + $panel->appendChild($form); + return $this->buildStandardPageResponse( + $panel, + array('title' => $title) + ); + } +} diff --git a/src/applications/oauthserver/controller/clientauthorization/edit/__init__.php b/src/applications/oauthserver/controller/clientauthorization/edit/__init__.php new file mode 100644 index 0000000000..4e0dfc10aa --- /dev/null +++ b/src/applications/oauthserver/controller/clientauthorization/edit/__init__.php @@ -0,0 +1,27 @@ +getRequest(); + $current_user = $request->getUser(); + $authorizations = id(new PhabricatorOAuthClientAuthorization()) + ->loadAllWhere('userPHID = %s', + $current_user->getPHID()); + + $client_authorizations = mpull($authorizations, null, 'getClientPHID'); + $client_phids = array_keys($client_authorizations); + if ($client_phids) { + $clients = id(new PhabricatorOAuthServerClient()) + ->loadAllWhere('phid in (%Ls)', + $client_phids); + } else { + $clients = array(); + } + $client_dict = mpull($clients, null, 'getPHID'); + + $rows = array(); + $rowc = array(); + $highlight = $this->getHighlightPHIDs(); + foreach ($client_authorizations as $client_phid => $authorization) { + $client = $client_dict[$client_phid]; + $created = phabricator_datetime($authorization->getDateCreated(), + $current_user); + $updated = phabricator_datetime($authorization->getDateModified(), + $current_user); + $row = array( + phutil_render_tag( + 'a', + array( + 'href' => $client->getViewURI(), + ), + phutil_escape_html($client->getName()) + ), + phutil_render_tag( + 'a', + array( + 'href' => 'TODO - link to scope about', + ), + $authorization->getScopeString() + ), + phabricator_datetime( + $authorization->getDateCreated(), + $current_user + ), + phabricator_datetime( + $authorization->getDateModified(), + $current_user + ), + phutil_render_tag( + 'a', + array( + 'class' => 'small button grey', + 'href' => $authorization->getEditURI(), + ), + 'Edit' + ), + ); + + $rows[] = $row; + if (isset($highlight[$authorization->getPHID()])) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = ''; + } + } + + $panel = $this->buildClientAuthorizationList($rows, $rowc, $title); + + return $this->buildStandardPageResponse( + array($this->getNoticeView(), + $panel), + array('title' => $title) + ); + } + + private function buildClientAuthorizationList($rows, $rowc, $title) { + $table = new AphrontTableView($rows); + $table->setRowClasses($rowc); + $table->setHeaders( + array( + 'Client', + 'Scope', + 'Created', + 'Updated', + '', + )); + $table->setColumnClasses( + array( + 'wide pri', + '', + '', + '', + 'action', + )); + if (empty($rows)) { + $table->setNoDataString( + 'You have not authorized any clients for this OAuthServer.' + ); + } + + $panel = new AphrontPanelView(); + $panel->appendChild($table); + $panel->setHeader($title); + + return $panel; + } + + private function getNoticeView() { + $edited = $this->getRequest()->getStr('edited'); + $deleted = $this->getRequest()->getBool('deleted'); + if ($edited) { + $edited = phutil_escape_html($edited); + $title = 'Successfully edited client authorization.'; + } else if ($deleted) { + $title = 'Successfully deleted client authorization.'; + } else { + $title = null; + } + + if ($title) { + $view = new AphrontErrorView(); + $view->setTitle($title); + $view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); + } else { + $view = null; + } + + return $view; + } +} diff --git a/src/applications/oauthserver/controller/clientauthorization/list/__init__.php b/src/applications/oauthserver/controller/clientauthorization/list/__init__.php new file mode 100644 index 0000000000..839fc58ea8 --- /dev/null +++ b/src/applications/oauthserver/controller/clientauthorization/list/__init__.php @@ -0,0 +1,21 @@ +getRequest(); $current_user = $request->getUser(); $server = new PhabricatorOAuthServer($current_user); + $panels = array(); + $results = array(); + - $forms = array(); - $form = id(new AphrontFormView()) - ->setUser($current_user) - ->appendChild( - id(new AphrontFormStaticControl()) - ->setValue('Create Test Client')) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Name') - ->setName('name') - ->setValue('')) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Redirect URI') - ->setName('redirect_uri') - ->setValue('')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue('Create Client')); - $forms[] = $form; - $result = array(); if ($request->isFormPost()) { - $name = $request->getStr('name'); - $redirect_uri = $request->getStr('redirect_uri'); - $secret = Filesystem::readRandomCharacters(32); - $client = new PhabricatorOAuthServerClient(); - $client->setName($name); - $client->setSecret($secret); - $client->setCreatorPHID($current_user->getPHID()); - $client->setRedirectURI($redirect_uri); - $client->save(); - $id = $client->getID(); - $phid = $client->getPHID(); - $name = phutil_escape_html($name); - $results = array(); - $results[] = "New client named {$name} with secret {$secret}."; - $results[] = "Client has id {$id} and phid {$phid}."; - $result = implode('
', $results); + $action = $request->getStr('action'); + switch ($action) { + case 'testclientauthorization': + $user_phid = $current_user->getPHID(); + $client_phid = $request->getStr('client_phid'); + $client = id(new PhabricatorOAuthServerClient) + ->loadOneWhere('phid = %s', $client_phid); + if (!$client) { + throw new Exception('Failed to load client!'); + } + if ($client->getCreatorPHID() != $user_phid || + $current_user->getPHID() != $user_phid) { + throw new Exception( + 'Only allowed to make test data for yourself '. + 'for clients you own!' + ); + } + // blankclientauthorizations don't get scope + $scope = array(); + $server->setUser($current_user); + $server->setClient($client); + $authorization = $server->authorizeClient($scope); + return id(new AphrontRedirectResponse()) + ->setURI('/oauthserver/clientauthorization/?edited='. + $authorization->getPHID()); + break; + default: + break; + } } - $title = 'Test OAuthServer Stuff'; - $panel = new AphrontPanelView(); - $panel->setWidth(AphrontPanelView::WIDTH_FORM); - $panel->setHeader($title); - $panel->appendChild($result); - $panel->appendChild($forms); - - return $this->buildStandardPageResponse( - $panel, - array('title' => $title)); } } diff --git a/src/applications/oauthserver/controller/test/__init__.php b/src/applications/oauthserver/controller/test/__init__.php index a35b9d9d81..5f44730348 100644 --- a/src/applications/oauthserver/controller/test/__init__.php +++ b/src/applications/oauthserver/controller/test/__init__.php @@ -6,17 +6,11 @@ -phutil_require_module('phabricator', 'applications/auth/controller/base'); +phutil_require_module('phabricator', 'aphront/response/redirect'); +phutil_require_module('phabricator', 'applications/oauthserver/controller/base'); phutil_require_module('phabricator', 'applications/oauthserver/server'); phutil_require_module('phabricator', 'applications/oauthserver/storage/client'); -phutil_require_module('phabricator', 'view/form/base'); -phutil_require_module('phabricator', 'view/form/control/static'); -phutil_require_module('phabricator', 'view/form/control/submit'); -phutil_require_module('phabricator', 'view/form/control/text'); -phutil_require_module('phabricator', 'view/layout/panel'); -phutil_require_module('phutil', 'filesystem'); -phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/oauthserver/scope/PhabricatorOAuthServerScope.php b/src/applications/oauthserver/scope/PhabricatorOAuthServerScope.php index c2cee2bc6c..22246549ab 100644 --- a/src/applications/oauthserver/scope/PhabricatorOAuthServerScope.php +++ b/src/applications/oauthserver/scope/PhabricatorOAuthServerScope.php @@ -34,4 +34,48 @@ final class PhabricatorOAuthServerScope { ); } + static public function getCheckboxControl($current_scopes) { + $scopes = self::getScopesDict(); + $scope_keys = array_keys($scopes); + sort($scope_keys); + + $checkboxes = new AphrontFormCheckboxControl(); + foreach ($scope_keys as $scope) { + $checkboxes->addCheckbox( + $name = $scope, + $value = 1, + $label = self::getCheckboxLabel($scope), + $checked = isset($current_scopes[$scope]) + ); + } + $checkboxes->setLabel('Scope'); + + return $checkboxes; + } + + static private function getCheckboxLabel($scope) { + $label = null; + switch ($scope) { + case self::SCOPE_OFFLINE_ACCESS: + $label = 'Make access tokens granted to this client never expire.'; + break; + case self::SCOPE_WHOAMI: + $label = 'Read access to Conduit method user.whoami.'; + break; + } + + return $label; + } + + static public function getScopesFromRequest(AphrontRequest $request) { + $scopes = self::getScopesDict(); + $requested_scopes = array(); + foreach ($scopes as $scope => $bit) { + if ($request->getBool($scope)) { + $requested_scopes[$scope] = 1; + } + } + return $requested_scopes; + } + } diff --git a/src/applications/oauthserver/scope/__init__.php b/src/applications/oauthserver/scope/__init__.php index cb94a7e09a..3fad220570 100644 --- a/src/applications/oauthserver/scope/__init__.php +++ b/src/applications/oauthserver/scope/__init__.php @@ -6,5 +6,7 @@ +phutil_require_module('phabricator', 'view/form/control/checkbox'); + phutil_require_source('PhabricatorOAuthServerScope.php'); diff --git a/src/applications/oauthserver/server/PhabricatorOAuthServer.php b/src/applications/oauthserver/server/PhabricatorOAuthServer.php index 4512b0e57d..d60e3c4323 100644 --- a/src/applications/oauthserver/server/PhabricatorOAuthServer.php +++ b/src/applications/oauthserver/server/PhabricatorOAuthServer.php @@ -108,6 +108,8 @@ final class PhabricatorOAuthServer { $authorization->setClientPHID($this->getClient()->getPHID()); $authorization->setScope($scope); $authorization->save(); + + return $authorization; } /** diff --git a/src/applications/oauthserver/storage/client/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/client/PhabricatorOAuthServerClient.php index 6e53ef9427..599b4f0b99 100644 --- a/src/applications/oauthserver/storage/client/PhabricatorOAuthServerClient.php +++ b/src/applications/oauthserver/storage/client/PhabricatorOAuthServerClient.php @@ -29,6 +29,18 @@ extends PhabricatorOAuthServerDAO { protected $redirectURI; protected $creatorPHID; + public function getEditURI() { + return '/oauthserver/client/edit/'.$this->getPHID().'/'; + } + + public function getViewURI() { + return '/oauthserver/client/view/'.$this->getPHID().'/'; + } + + public function getDeleteURI() { + return '/oauthserver/client/delete/'.$this->getPHID().'/'; + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, diff --git a/src/applications/oauthserver/storage/clientauthorization/PhabricatorOAuthClientAuthorization.php b/src/applications/oauthserver/storage/clientauthorization/PhabricatorOAuthClientAuthorization.php index f35c87278e..f5f627d279 100644 --- a/src/applications/oauthserver/storage/clientauthorization/PhabricatorOAuthClientAuthorization.php +++ b/src/applications/oauthserver/storage/clientauthorization/PhabricatorOAuthClientAuthorization.php @@ -28,6 +28,21 @@ extends PhabricatorOAuthServerDAO { protected $clientPHID; protected $scope; + public function getEditURI() { + return '/oauthserver/clientauthorization/edit/'.$this->getPHID().'/'; + } + + public function getDeleteURI() { + return '/oauthserver/clientauthorization/delete/'.$this->getPHID().'/'; + } + + public function getScopeString() { + $scope = $this->getScope(); + $scopes = array_keys($scope); + sort($scopes); + return implode(', ', $scopes); + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, diff --git a/src/view/layout/panel/AphrontPanelView.php b/src/view/layout/panel/AphrontPanelView.php index 9ca537d474..d0e0049561 100644 --- a/src/view/layout/panel/AphrontPanelView.php +++ b/src/view/layout/panel/AphrontPanelView.php @@ -95,7 +95,10 @@ final class AphrontPanelView extends AphrontView { implode(" ", $this->buttons). ''; } - + $header_elements = + '
'. + $buttons.$header.$caption. + '
'; $table = $this->renderChildren(); require_celerity_resource('aphront-panel-view-css'); @@ -112,7 +115,7 @@ final class AphrontPanelView extends AphrontView { 'class' => implode(' ', $classes), 'id' => $this->id, ), - $buttons.$header.$caption.$table); + $header_elements.$table); } } diff --git a/webroot/rsrc/css/aphront/form-view.css b/webroot/rsrc/css/aphront/form-view.css index bcae93e833..864e0eaf1a 100644 --- a/webroot/rsrc/css/aphront/form-view.css +++ b/webroot/rsrc/css/aphront/form-view.css @@ -99,7 +99,8 @@ } -.aphront-form-control-static .aphront-form-input { +.aphront-form-control-static .aphront-form-input, +.aphront-form-control-markup .aphront-form-input { padding-top: 4px; font-size: 13px; } diff --git a/webroot/rsrc/css/aphront/panel-view.css b/webroot/rsrc/css/aphront/panel-view.css index 7dbab52f12..3df87853c4 100644 --- a/webroot/rsrc/css/aphront/panel-view.css +++ b/webroot/rsrc/css/aphront/panel-view.css @@ -11,16 +11,20 @@ margin: 1em 2em; } +.aphront-panel-view .aphront-panel-header { + margin: 0 0 1em 0; +} + .aphront-panel-view h1 { font-size: 14px; font-weight: bold; - padding: 2px 0 8px; + padding: 4px 0 0 0; } .aphront-panel-view-caption { font-size: 11px; color: #666666; - margin-top: -0.75em; + margin-top: -0.1em; margin-bottom: 0.75em; }