diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php index bb0b060940..e980b5cefe 100755 --- a/scripts/user/account_admin.php +++ b/scripts/user/account_admin.php @@ -1,226 +1,226 @@ #!/usr/bin/env php establishConnection('r'), 'SELECT * FROM %T LIMIT 1', $table->getTableName()); $is_first_user = (!$any_user); if ($is_first_user) { echo pht( "WARNING\n\n". "You're about to create the first account on this install. Normally, you ". "should use the web interface to create the first account, not this ". "script.\n\n". "If you use the web interface, it will drop you into a nice UI workflow ". "which gives you more help setting up your install. If you create an ". "account with this script instead, you will skip the setup help and you ". "will not be able to access it later."); if (!phutil_console_confirm(pht("Skip easy setup and create account?"))) { echo pht("Cancelled.")."\n"; exit(1); } } echo "Enter a username to create a new account or edit an existing account."; $username = phutil_console_prompt("Enter a username:"); if (!strlen($username)) { echo "Cancelled.\n"; exit(1); } if (!PhabricatorUser::validateUsername($username)) { $valid = PhabricatorUser::describeValidUsername(); echo "The username '{$username}' is invalid. {$valid}\n"; exit(1); } $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $username); if (!$user) { $original = new PhabricatorUser(); echo "There is no existing user account '{$username}'.\n"; $ok = phutil_console_confirm( "Do you want to create a new '{$username}' account?", $default_no = false); if (!$ok) { echo "Cancelled.\n"; exit(1); } $user = new PhabricatorUser(); $user->setUsername($username); $is_new = true; } else { $original = clone $user; echo "There is an existing user account '{$username}'.\n"; $ok = phutil_console_confirm( "Do you want to edit the existing '{$username}' account?", $default_no = false); if (!$ok) { echo "Cancelled.\n"; exit(1); } $is_new = false; } $user_realname = $user->getRealName(); if (strlen($user_realname)) { $realname_prompt = ' ['.$user_realname.']'; } else { $realname_prompt = ''; } $realname = nonempty( phutil_console_prompt("Enter user real name{$realname_prompt}:"), $user_realname); $user->setRealName($realname); // When creating a new user we prompt for an email address; when editing an // existing user we just skip this because it would be quite involved to provide // a reasonable CLI interface for editing multiple addresses and managing email // verification and primary addresses. $create_email = null; if ($is_new) { do { $email = phutil_console_prompt("Enter user email address:"); $duplicate = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $email); if ($duplicate) { echo "ERROR: There is already a user with that email address. ". "Each user must have a unique email address.\n"; } else { break; } } while (true); $create_email = $email; } $changed_pass = false; // This disables local echo, so the user's password is not shown as they type // it. phutil_passthru('stty -echo'); $password = phutil_console_prompt( "Enter a password for this user [blank to leave unchanged]:"); phutil_passthru('stty echo'); if (strlen($password)) { $changed_pass = $password; } $is_system_agent = $user->getIsSystemAgent(); $set_system_agent = phutil_console_confirm( - 'Should this user be a system agent?', + 'Is this user a bot/script?', $default_no = !$is_system_agent); $verify_email = null; $set_verified = false; // Allow administrators to verify primary email addresses at this time in edit // scenarios. (Create will work just fine from here as we auto-verify email // on create.) if (!$is_new) { $verify_email = $user->loadPrimaryEmail(); if (!$verify_email->getIsVerified()) { $set_verified = phutil_console_confirm( 'Should the primary email address be verified?', $default_no = true); } else { // already verified so let's not make a fuss $verify_email = null; } } $is_admin = $user->getIsAdmin(); $set_admin = phutil_console_confirm( 'Should this user be an administrator?', $default_no = !$is_admin); echo "\n\nACCOUNT SUMMARY\n\n"; $tpl = "%12s %-30s %-30s\n"; printf($tpl, null, 'OLD VALUE', 'NEW VALUE'); printf($tpl, 'Username', $original->getUsername(), $user->getUsername()); printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName()); if ($is_new) { printf($tpl, 'Email', '', $create_email); } printf($tpl, 'Password', null, ($changed_pass !== false) ? 'Updated' : 'Unchanged'); printf( $tpl, - 'System Agent', + 'Bot/Script', $original->getIsSystemAgent() ? 'Y' : 'N', $set_system_agent ? 'Y' : 'N'); if ($verify_email) { printf( $tpl, 'Verify Email', $verify_email->getIsVerified() ? 'Y' : 'N', $set_verified ? 'Y' : 'N'); } printf( $tpl, 'Admin', $original->getIsAdmin() ? 'Y' : 'N', $set_admin ? 'Y' : 'N'); echo "\n"; if (!phutil_console_confirm("Save these changes?", $default_no = false)) { echo "Cancelled.\n"; exit(1); } $user->openTransaction(); $editor = new PhabricatorUserEditor(); // TODO: This is wrong, but we have a chicken-and-egg problem when you use // this script to create the first user. $editor->setActor($user); if ($is_new) { $email = id(new PhabricatorUserEmail()) ->setAddress($create_email) ->setIsVerified(1); // Unconditionally approve new accounts created from the CLI. $user->setIsApproved(1); $editor->createNewUser($user, $email); } else { if ($verify_email) { $user->setIsEmailVerified(1); $verify_email->setIsVerified($set_verified ? 1 : 0); } $editor->updateUser($user, $verify_email); } $editor->makeAdminUser($user, $set_admin); $editor->makeSystemAgentUser($user, $set_system_agent); if ($changed_pass !== false) { $envelope = new PhutilOpaqueEnvelope($changed_pass); $editor->changePassword($user, $envelope); } $user->saveTransaction(); echo "Saved changes.\n"; diff --git a/src/applications/people/controller/PhabricatorPeopleListController.php b/src/applications/people/controller/PhabricatorPeopleListController.php index 63b7c21c89..b0db5d7f62 100644 --- a/src/applications/people/controller/PhabricatorPeopleListController.php +++ b/src/applications/people/controller/PhabricatorPeopleListController.php @@ -1,110 +1,110 @@ key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $this->requireApplicationCapability( PeopleCapabilityBrowseUserDirectory::CAPABILITY); $controller = id(new PhabricatorApplicationSearchController($request)) ->setQueryKey($this->key) ->setSearchEngine(new PhabricatorPeopleSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function renderResultsList( array $users, PhabricatorSavedQuery $query) { assert_instances_of($users, 'PhabricatorUser'); $request = $this->getRequest(); $viewer = $request->getUser(); $list = new PHUIObjectItemListView(); $is_approval = ($query->getQueryKey() == 'approval'); foreach ($users as $user) { $primary_email = $user->loadPrimaryEmail(); if ($primary_email && $primary_email->getIsVerified()) { $email = pht('Verified'); } else { $email = pht('Unverified'); } $item = new PHUIObjectItemView(); $item->setHeader($user->getFullName()) ->setHref('/p/'.$user->getUsername().'/') ->addAttribute(hsprintf('%s %s', phabricator_date($user->getDateCreated(), $viewer), phabricator_time($user->getDateCreated(), $viewer))) ->addAttribute($email) ->setImageURI($user->getProfileImageURI()); if ($is_approval && $primary_email) { $item->addAttribute($primary_email->getAddress()); } if ($user->getIsDisabled()) { $item->addIcon('disable', pht('Disabled')); } if (!$is_approval) { if (!$user->getIsApproved()) { $item->addIcon('perflab-grey', pht('Needs Approval')); } } if ($user->getIsAdmin()) { $item->addIcon('highlight', pht('Admin')); } if ($user->getIsSystemAgent()) { - $item->addIcon('computer', pht('System Agent')); + $item->addIcon('computer', pht('Bot/Script')); } if ($viewer->getIsAdmin()) { $user_id = $user->getID(); if ($is_approval) { $item->addAction( id(new PHUIListItemView()) ->setIcon('disable') ->setName(pht('Disable')) ->setWorkflow(true) ->setHref($this->getApplicationURI('disapprove/'.$user_id.'/'))); $item->addAction( id(new PHUIListItemView()) ->setIcon('like') ->setName(pht('Approve')) ->setWorkflow(true) ->setHref($this->getApplicationURI('approve/'.$user_id.'/'))); } } $list->addItem($item); } return $list; } } diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php index c127470937..9b5ea5337d 100644 --- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php @@ -1,173 +1,173 @@ setParameter('usernames', $request->getStrList('usernames')); $saved->setParameter('nameLike', $request->getStr('nameLike')); $saved->setParameter('isAdmin', $request->getStr('isAdmin')); $saved->setParameter('isDisabled', $request->getStr('isDisabled')); $saved->setParameter('isSystemAgent', $request->getStr('isSystemAgent')); $saved->setParameter('needsApproval', $request->getStr('needsApproval')); $saved->setParameter('createdStart', $request->getStr('createdStart')); $saved->setParameter('createdEnd', $request->getStr('createdEnd')); $this->readCustomFieldsFromRequest($request, $saved); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorPeopleQuery()) ->needPrimaryEmail(true) ->needProfileImage(true); $usernames = $saved->getParameter('usernames', array()); if ($usernames) { $query->withUsernames($usernames); } $like = $saved->getParameter('nameLike'); if ($like) { $query->withNameLike($like); } $is_admin = $saved->getParameter('isAdmin'); $is_disabled = $saved->getParameter('isDisabled'); $is_system_agent = $saved->getParameter('isSystemAgent'); $needs_approval = $saved->getParameter('needsApproval'); $no_disabled = $saved->getParameter('noDisabled'); if ($is_admin) { $query->withIsAdmin(true); } if ($is_disabled) { $query->withIsDisabled(true); } else if ($no_disabled) { $query->withIsDisabled(false); } if ($is_system_agent) { $query->withIsSystemAgent(true); } if ($needs_approval) { $query->withIsApproved(false); } $start = $this->parseDateTime($saved->getParameter('createdStart')); $end = $this->parseDateTime($saved->getParameter('createdEnd')); if ($start) { $query->withDateCreatedAfter($start); } if ($end) { $query->withDateCreatedBefore($end); } $this->applyCustomFieldsToQuery($query, $saved); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $usernames = $saved->getParameter('usernames', array()); $like = $saved->getParameter('nameLike'); $is_admin = $saved->getParameter('isAdmin'); $is_disabled = $saved->getParameter('isDisabled'); $is_system_agent = $saved->getParameter('isSystemAgent'); $needs_approval = $saved->getParameter('needsApproval'); $form ->appendChild( id(new AphrontFormTextControl()) ->setName('usernames') ->setLabel(pht('Usernames')) ->setValue(implode(', ', $usernames))) ->appendChild( id(new AphrontFormTextControl()) ->setName('nameLike') ->setLabel(pht('Name Contains')) ->setValue($like)) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel('Role') ->addCheckbox( 'isAdmin', 1, - pht('Show only Administrators.'), + pht('Show only administrators.'), $is_admin) ->addCheckbox( 'isDisabled', 1, pht('Show only disabled users.'), $is_disabled) ->addCheckbox( 'isSystemAgent', 1, - pht('Show only System Agents.'), + pht('Show only bots.'), $is_system_agent) ->addCheckbox( 'needsApproval', 1, pht('Show only users who need approval.'), $needs_approval)); $this->appendCustomFieldsToForm($form, $saved); $this->buildDateRange( $form, $saved, 'createdStart', pht('Joined After'), 'createdEnd', pht('Joined Before')); } protected function getURI($path) { return '/people/'.$path; } public function getBuiltinQueryNames() { $names = array( 'all' => pht('All'), ); $viewer = $this->requireViewer(); if ($viewer->getIsAdmin()) { $names['approval'] = pht('Approval Queue'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'approval': return $query ->setParameter('needsApproval', true) ->setParameter('noDisabled', true); } return parent::buildSavedQueryFromBuiltin($query_key); } } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php index 5c7ec10629..78f2c9ee8a 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php @@ -1,433 +1,433 @@ type = $data['type']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); $raw_query = $request->getStr('raw'); $need_rich_data = false; $need_users = false; $need_agents = false; $need_applications = false; $need_lists = false; $need_projs = false; $need_repos = false; $need_packages = false; $need_upforgrabs = false; $need_arcanist_projects = false; $need_noproject = false; $need_symbols = false; $need_jump_objects = false; $need_build_plans = false; $need_task_priority = false; $need_macros = false; $need_legalpad_documents = false; switch ($this->type) { case 'mainsearch': $need_users = true; $need_applications = true; $need_rich_data = true; $need_symbols = true; $need_projs = true; $need_jump_objects = true; break; case 'searchowner': $need_users = true; $need_upforgrabs = true; break; case 'searchproject': $need_projs = true; $need_noproject = true; break; case 'users': case 'accounts': case 'authors': $need_users = true; break; case 'mailable': case 'allmailable': $need_users = true; $need_lists = true; $need_projs = true; break; case 'projects': $need_projs = true; break; case 'usersorprojects': case 'accountsorprojects': $need_users = true; $need_projs = true; break; case 'repositories': $need_repos = true; break; case 'packages': $need_packages = true; break; case 'arcanistprojects': $need_arcanist_projects = true; break; case 'buildplans': $need_build_plans = true; break; case 'taskpriority': $need_task_priority = true; break; case 'macros': $need_macros = true; break; case 'legalpaddocuments': $need_legalpad_documents = true; break; } $results = array(); if ($need_upforgrabs) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('upforgrabs (Up For Grabs)') ->setPHID(ManiphestTaskOwner::OWNER_UP_FOR_GRABS); } if ($need_noproject) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('noproject (No Project)') ->setPHID(ManiphestTaskOwner::PROJECT_NO_PROJECT); } if ($need_users) { $columns = array( 'isSystemAgent', 'isAdmin', 'isDisabled', 'userName', 'realName', 'phid'); if ($query) { // This is an arbitrary limit which is just larger than any limit we // actually use in the application. // TODO: The datasource should pass this in the query. $limit = 15; $user_table = new PhabricatorUser(); $conn_r = $user_table->establishConnection('r'); $ids = queryfx_all( $conn_r, 'SELECT id FROM %T WHERE username LIKE %> ORDER BY username ASC LIMIT %d', $user_table->getTableName(), $query, $limit); $ids = ipull($ids, 'id'); if (count($ids) < $limit) { // If we didn't find enough username hits, look for real name hits. // We need to pull the entire pagesize so that we end up with the // right number of items if this query returns many duplicate IDs // that we've already selected. $realname_ids = queryfx_all( $conn_r, 'SELECT DISTINCT userID FROM %T WHERE token LIKE %> ORDER BY token ASC LIMIT %d', PhabricatorUser::NAMETOKEN_TABLE, $query, $limit); $realname_ids = ipull($realname_ids, 'userID'); $ids = array_merge($ids, $realname_ids); $ids = array_unique($ids); $ids = array_slice($ids, 0, $limit); } // Always add the logged-in user because some tokenizers autosort them // first. They'll be filtered out on the client side if they don't // match the query. $ids[] = $request->getUser()->getID(); if ($ids) { $users = id(new PhabricatorUser())->loadColumnsWhere( $columns, 'id IN (%Ld)', $ids); } else { $users = array(); } } else { $users = id(new PhabricatorUser())->loadColumns($columns); } if ($need_rich_data) { $phids = mpull($users, 'getPHID'); $handles = $this->loadViewerHandles($phids); } foreach ($users as $user) { $closed = null; if ($user->getIsDisabled()) { $closed = pht('Disabled'); } else if ($user->getIsSystemAgent()) { - $closed = pht('System Agent'); + $closed = pht('Bot/Script'); } $result = id(new PhabricatorTypeaheadResult()) ->setName($user->getFullName()) ->setURI('/p/'.$user->getUsername()) ->setPHID($user->getPHID()) ->setPriorityString($user->getUsername()) ->setIcon('policy-all') ->setPriorityType('user') ->setClosed($closed); if ($need_rich_data) { $display_type = 'User'; if ($user->getIsAdmin()) { $display_type = 'Administrator'; } $result->setDisplayType($display_type); $result->setImageURI($handles[$user->getPHID()]->getImageURI()); } $results[] = $result; } } if ($need_lists) { $lists = id(new PhabricatorMailingListQuery()) ->setViewer($viewer) ->execute(); foreach ($lists as $list) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($list->getName()) ->setURI($list->getURI()) ->setPHID($list->getPHID()); } } if ($need_build_plans) { $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->execute(); foreach ($plans as $plan) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($plan->getName()) ->setPHID($plan->getPHID()); } } if ($need_task_priority) { $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($priority_map as $value => $name) { // NOTE: $value is not a phid but is unique. This'll work. $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($value) ->setName($name); } } if ($need_macros) { $macros = id(new PhabricatorMacroQuery()) ->setViewer($viewer) ->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE) ->execute(); $macros = mpull($macros, 'getName', 'getPHID'); foreach ($macros as $phid => $name) { $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($phid) ->setName($name); } } if ($need_legalpad_documents) { $documents = id(new LegalpadDocumentQuery()) ->setViewer($viewer) ->execute(); $documents = mpull($documents, 'getTitle', 'getPHID'); foreach ($documents as $phid => $title) { $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($phid) ->setName($title); } } if ($need_projs) { $projs = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->needImages(true) ->execute(); foreach ($projs as $proj) { $closed = null; if ($proj->isArchived()) { $closed = pht('Archived'); } $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($proj->getName()) ->setDisplayType("Project") ->setURI('/project/view/'.$proj->getID().'/') ->setPHID($proj->getPHID()) ->setIcon('policy-project') ->setClosed($closed); $proj_result->setImageURI($proj->getProfileImageURI()); $results[] = $proj_result; } } if ($need_repos) { $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->execute(); foreach ($repos as $repo) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('r'.$repo->getCallsign().' ('.$repo->getName().')') ->setURI('/diffusion/'.$repo->getCallsign().'/') ->setPHID($repo->getPHID()) ->setPriorityString('r'.$repo->getCallsign()); } } if ($need_packages) { $packages = id(new PhabricatorOwnersPackage())->loadAll(); foreach ($packages as $package) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($package->getName()) ->setURI('/owners/package/'.$package->getID().'/') ->setPHID($package->getPHID()); } } if ($need_arcanist_projects) { $arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll(); foreach ($arcprojs as $proj) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($proj->getName()) ->setPHID($proj->getPHID()); } } if ($need_applications) { $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { $uri = $application->getTypeaheadURI(); if (!$uri) { continue; } $name = $application->getName().' '.$application->getShortDescription(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($uri) ->setPHID($application->getPHID()) ->setPriorityString($application->getName()) ->setDisplayName($application->getName()) ->setDisplayType($application->getShortDescription()) ->setImageuRI($application->getIconURI()) ->setPriorityType('apps'); } } if ($need_symbols) { $symbols = id(new DiffusionSymbolQuery()) ->setNamePrefix($query) ->setLimit(15) ->needArcanistProjects(true) ->needRepositories(true) ->needPaths(true) ->execute(); foreach ($symbols as $symbol) { $lang = $symbol->getSymbolLanguage(); $name = $symbol->getSymbolName(); $type = $symbol->getSymbolType(); $proj = $symbol->getArcanistProject()->getName(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($symbol->getURI()) ->setPHID(md5($symbol->getURI())) // Just needs to be unique. ->setDisplayName($name) ->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$proj.')') ->setPriorityType('symb'); } } if ($need_jump_objects) { $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($raw_query)) ->execute(); if ($objects) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($objects, 'getPHID')) ->execute(); $handle = head($handles); if ($handle) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($handle->getFullName()) ->setDisplayType($handle->getTypeName()) ->setURI($handle->getURI()) ->setPHID($handle->getPHID()) ->setPriorityType('jump'); } } } $content = mpull($results, 'getWireFormat'); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } // If there's a non-Ajax request to this endpoint, show results in a tabular // format to make it easier to debug typeahead output. $rows = array(); foreach ($results as $result) { $wire = $result->getWireFormat(); $rows[] = $wire; } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Name', 'URI', 'PHID', 'Priority', 'Display Name', 'Display Type', 'Image URI', 'Priority Type', )); $panel = new AphrontPanelView(); $panel->setHeader('Typeahead Results'); $panel->appendChild($table); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Typeahead Results', )); } } diff --git a/src/docs/tech/chatbot.diviner b/src/docs/tech/chatbot.diviner index bd095fb939..a372e14295 100644 --- a/src/docs/tech/chatbot.diviner +++ b/src/docs/tech/chatbot.diviner @@ -1,84 +1,84 @@ @title Chat Bot Technical Documentation @group bot Configuring and extending the chat bot. = Overview = Phabricator includes a simple chat bot daemon, which is primarily intended as an example of how you can write an external script that interfaces with Phabricator over Conduit and does some kind of useful work. If you use IRC or another supported chat protocol, you can also have the bot hang out in your channel. NOTE: The chat bot is somewhat experimental and not very mature. = Configuring the Bot = The bot reads a JSON configuration file. You can find an example in: resources/chatbot/example_config.json These are the configuration values it reads: - ##server## String, required, the server to connect to. - ##port## Int, optional, the port to connect to (defaults to 6667). - ##ssl## Bool, optional, whether to connect via SSL or not (defaults to false). - ##nick## String, nickname to use. - ##user## String, optional, username to use (defaults to ##nick##). - ##pass## String, optional, password for server. - ##nickpass## String, optional, password for NickServ. - ##join## Array, list of channels to join. - ##handlers## Array, list of handlers to run. These are like plugins for the bot. - ##conduit.uri##, ##conduit.user##, ##conduit.cert## Conduit configuration, see below. - ##notification.channels## Notification configuration, see below. = Handlers = You specify a list of "handlers", which are basically plugins or modules for the bot. These are the default handlers available: - @{class:PhabricatorBotObjectNameHandler} This handler looks for users mentioning Phabricator objects like "T123" and "D345" in chat, looks them up, and says their name with a link to the object. Requires conduit. - @{class:PhabricatorBotDifferentialNotificationHandler} This handler posts notifications about changes to revisions to the channels listed in ##notification.channels##. - @{class:PhabricatorBotLogHandler} This handler records chatlogs which can be browsed in the Phabricator web interface. You can also write your own handlers, by extending @{class:PhabricatorBotHandler}. = Conduit = Some handlers (e.g., @{class:PhabricatorBotObjectNameHandler}) need to read data from Phabricator over Conduit, Phabricator's HTTP API. You can use this method to allow other scripts or programs to access Phabricator's data from different servers and in different languages. To allow the bot to access Conduit, you need to create a user that it can login with. To do this, login to Phabricator as an administrator and go to ##People -> Create New Account##. Create a new account and flag them as a -"System Agent". Then in your configuration file, set these parameters: +"Bot/Script". Then in your configuration file, set these parameters: - ##conduit.uri## The URI for your Phabricator install, like ##http://phabricator.example.com/## - ##conduit.user## The username your bot should login to Phabricator with -- whatever you selected above, like ##phabot##. - ##conduit.cert## The user's certificate, from the "Conduit Certificate" tab in the user's administrative view. Now the bot should be able to connect to Phabricator via Conduit. = Starting the Bot = The bot is a Phabricator daemon, so start it with ##phd##: ./bin/phd launch phabricatorbot If you have issues you can try ##debug## instead of ##launch##, see @{article:Managing Daemons with phd} for more information. diff --git a/src/docs/user/userguide/users.diviner b/src/docs/user/userguide/users.diviner index 3de5958a9e..8d1e28aedb 100644 --- a/src/docs/user/userguide/users.diviner +++ b/src/docs/user/userguide/users.diviner @@ -1,66 +1,75 @@ @title User Guide: Account Roles @group userguide -Describes account roles like "Administrator", "Disabled" and "System Agent". +Describes account roles like "Administrator", "Disabled" and "Bot". = Overview = When you create a user account, you can set roles like "Administrator", -"Disabled" or "System Agent". This document explains what these roles mean. +"Disabled" or "Bot". This document explains what these roles mean. = Administrators = -**Administrators** are normal users with extra capabilities. They have access -to some tools and workflows that normal users don't, which they can use to -debug and configure Phabricator. For example, they have access to: +**Administrators** are normal users with a few extra capabilities. Their primary +role is to keep things running smoothly, and they are not all-powerful. In +Phabricator, administrators are more like //janitors//. - - **Account Management**: The primary function of administrators is adding, - disabling, and managing user accounts. Administrators can create and edit - accounts and view access logs. - - **Repositories**: Administrators can configure repositories. This isn't - normally available because it is specialized and complicated to configure. - -Administrators have a few other minor capabilities in other tools. When you are -in an administrative interface, the menu bar is red. +Administrators can create, delete, enable, disable, and approve user accounts. +Various applications have a few other capabilities which are reserved for +administrators by default, but these can be changed to provide access to more +or fewer users. Administrators are **not** in complete control of the system. Administrators -**can not** login as other users or act on behalf of other users. Administrators -**can not** bypass object privacy policies. +**can not** login as other users or act on behalf of other users. They can not +destroy data or make changes without leaving an audit trail. Administrators also +can not bypass object privacy policies. + +Limiting the power of administrators means that administrators can't abuse +their power (they have very little power to abuse), a malicious administrator +can't do much damage, and an attacker who compromises an administrator account +is limited in what they can accomplish. NOTE: Administrators currently //can// act on behalf of other users via Conduit. This will be locked down at some point. -= System Agents = += Bot/Script Accounts = + +**Bot/Script** accounts are accounts for bots and scripts which need to +interface with the system, but are not regular users. Generally, when you write +scripts that use Conduit (like the IRC bot), you should create a Bot/Script +account for them. + +These accounts were previously called "System Agents", but were renamed to make +things more clear. -**System Agents** are accounts for bots and scripts which need to interface -with the system but are not regular users. Generally, when you write scripts -that use Conduit (like the IRC bot), you should create a System Agent account -for them. System agents: +The **Bot/Script** role for an account can not be changed after the account is +created. This prevents administrators form changing a normal user into a bot, +retrieving their Conduit certificate, and then changing them back (which +would allow administrators to gain other users' credentials). - - **can not login** (they //can// access API methods via Conduit); - - **can not review diffs or own tasks**; - - **do not appear in CC tokenzers**. +**Bot/Script** accounts differ from normal accounts in that: -Currently, the **System Agent** role for an account can not be changed after the -account is created. This prevents administrators form changing a normal user -into a system agent, retrieving their Conduit certificate, and then changing -them back (which would allow administrators to gain other users' credentials). + - administrators can access them, edit settings, and retrieve credentials; + - they do not receive email; + - they appear with lower precedence in the UI when selecting users, with + a "Bot" note (because i t usually does not make sense to, for example, + assign a task to a bot). = Disabled Users = **Disabled Users** are accounts that are no longer active. Generally, when someone leaves a project (e.g., leaves your company, or their internship or contract ends) you should disable their account to terminate their access to the system. Disabled users: - - **can not login**; - - **can not access Conduit**; - - **do not receive email**; - - **do not appear in owner/reviewer/CC tokenizers**. - -Users can only be disabled (not deleted) because there are a number of workflows -that don't make sense if their account is completely deleted, like: finding old -revisions or tasks that they were responsible for (so you can get someone else -to take care of them); identifying them as the author of their changes; and -restoring all their data if they rejoin the project (e.g., they are later -re-hired, maybe as a full time employee after an internship). + - can not login; + - can not access Conduit; + - do not receive email; and + - appear with lower precedence in the UI when selecting users, with a + "Disabled" note (because it usually does not make sense to, for example, + assign a task to a disabled user). + +While users can also be deleted, it is strongly recommended that you disable +them instead if they interacted with any objects in the system. If you delete a +user entirely, you won't be able to find things they used to own or restore +their data later if they rejoin the project.