diff --git a/README.markdown b/README.markdown index 19d8405..2c5a464 100644 --- a/README.markdown +++ b/README.markdown @@ -1,8 +1,12 @@ # phpBB Directory Server for Atlassian Crowd -The phpBB Directory Server is in com.phpbb.crowd.phpBBDirectoryServer. It implements com.atlassian.crowd.integration.directory.RemoteDirectory and can easily be configured as a Directory in Crowd. It does not support any write operations. +The phpBB Directory Server is in `com.phpbb.crowd.phpBBDirectoryServer`. It implements `com.atlassian.crowd.integration.directory.RemoteDirectory` and can easily be configured as a Directory in Crowd. It does not support any write operations. + +## Requirements +* Crowd: 3.3.3+ +* phpBB: 3.3.0+ ## Installing and Configuring the phpBB Directory Server -Install the Atlassian SDK. Using its atlas-package command you can easily build the phpbbauth jar file from the crowd-phpbb-auth directory. Place this jar file into crowd/webapp/WEB-INF/lib. Make sure to export the CROWD_PHPBB_ROOT_URL environment variable, it needs to contain the URL to the root of your phpBB board. Install the phpBB External Authentication API. In Crowd there is a top level Directories tab. Here you can add a new directory. In the "Implementation Class" field you have to fill in "com.phpbb.crowd.phpBBDirectoryServer". You should disable all possible write operations in the Permissions tab - even if you don't, all write operations will throw Exceptions. The directory server also does not support any attributes. So even if they show up in the GUI they will not be saved by phpBB. +Install the Atlassian SDK. Using its `atlas-package` command you can easily build the phpbbauth jar file from the `crowd-phpbb-auth` directory. Place this jar file into `crowd/webapp/WEB-INF/lib`. Make sure to export the `CROWD_PHPBB_ROOT_URL` environment variable, it needs to contain the URL to the root of your phpBB board. Install the phpBB External Authentication API. In Crowd there is a top level Directories tab. Here you can add a new directory. In the "Implementation Class" field you have to fill in "`com.phpbb.crowd.phpBBDirectoryServer`". You should disable all possible write operations in the Permissions tab - even if you don't, all write operations will throw Exceptions. The directory server also does not support any attributes. So even if they show up in the GUI they will not be saved by phpBB. # phpBB External Authentication API -The API is rather simple for now. It consists of a single PHP file, to be placed in the phpBB root directory. The file replies to a number of POST requests. You can ask it to authenticate a user, password pair against the selected authentication method. You can query the user and group tables individually or join them to retrieve information about group memberships. The interface needs to be cleaned up after which it will make more sense to document all the functionality. \ No newline at end of file +The API is rather simple for now. It consists of a single PHP file, to be placed in the phpBB root directory. Copy the values from `config.dist.php` into your board's `config.php`. The file replies to a number of POST requests. You can ask it to authenticate a user, password pair against the selected authentication method. You can query the user and group tables individually or join them to retrieve information about group memberships. The interface needs to be cleaned up after which it will make more sense to document all the functionality. \ No newline at end of file diff --git a/crowd-phpbb-directory-server/pom.xml b/crowd-phpbb-directory-server/pom.xml index 0e5b7a0..5bcf4f5 100644 --- a/crowd-phpbb-directory-server/pom.xml +++ b/crowd-phpbb-directory-server/pom.xml @@ -7,53 +7,87 @@ 4.0.0 phpbb phpbbauth - 1.2-SNAPSHOT + 1.3-SNAPSHOT phpBB - http://www.phpbb.com/ + https://www.phpBB.com/ phpbbauth This is the phpbb:phpbbauth plugin for Atlassian Crowd. jar + + + + com.atlassian.crowd + atlassian-crowd + ${crowd.version} + pom + import + + + + - - junit - junit - 4.6 - test - com.atlassian.crowd crowd-api - 2.4.0 - provided - - - com.atlassian.crowd - crowd-integration-api - 2.4.0 - provided - maven-compiler-plugin + com.atlassian.maven.plugins + crowd-maven-plugin + ${amps.version} + true - 1.6 - 1.6 + ${crowd.version} + ${crowd.data.version} + true + + + + + ${atlassian.plugin.key} + + + + com.phpbb.crowd, + + + + * + + + -Xlint:all + true + - 2.4.0 - 2.4.0 + 3.3.3 + 3.3.3 + 8.0.0 + + ${project.groupId}.${project.artifactId} + UTF-8 + 1.8 + 1.8 diff --git a/crowd-phpbb-directory-server/src/main/java/com/phpbb/crowd/phpBBDirectoryServer.java b/crowd-phpbb-directory-server/src/main/java/com/phpbb/crowd/phpBBDirectoryServer.java index e3a4ded..bfc5c0c 100644 --- a/crowd-phpbb-directory-server/src/main/java/com/phpbb/crowd/phpBBDirectoryServer.java +++ b/crowd-phpbb-directory-server/src/main/java/com/phpbb/crowd/phpBBDirectoryServer.java @@ -15,8 +15,8 @@ */ package com.phpbb.crowd; -import com.atlassian.crowd.embedded.api.PasswordCredential; import com.atlassian.crowd.directory.RemoteDirectory; +import com.atlassian.crowd.embedded.api.PasswordCredential; import com.atlassian.crowd.exception.*; import com.atlassian.crowd.model.*; import com.atlassian.crowd.model.user.*; @@ -28,11 +28,13 @@ import com.atlassian.crowd.search.ReturnType; import com.atlassian.crowd.search.Entity; import com.atlassian.crowd.embedded.api.SearchRestriction; +import com.atlassian.crowd.util.BoundedCount; import java.rmi.RemoteException; import java.util.*; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.*; import java.net.*; @@ -45,7 +47,7 @@ */ public class phpBBDirectoryServer implements RemoteDirectory { - private static final Logger log = Logger.getLogger(phpBBDirectoryServer.class); + private static final Logger log = LoggerFactory.getLogger(phpBBDirectoryServer.class); private long directoryId; private Map attributes; @@ -63,11 +65,13 @@ public phpBBDirectoryServer() serverUrl = System.getenv("CROWD_PHPBB_ROOT_URL"); } + @Override public long getDirectoryId() { return directoryId; } + @Override public void setDirectoryId(long directoryId) { this.directoryId = directoryId; @@ -78,28 +82,33 @@ public String getName() return "phpbbauth"; } + @Override public String getDescriptiveName() { return "phpBB Directory Server"; } + @Override public void setAttributes(Map attributes) { this.attributes = attributes; } + @Override public Set getValues(String name) { log.info("crowd-phpbbauth-plugin: getAttributes: " + name); return new HashSet(); } + @Override public String getValue(String name) { log.info("crowd-phpbbauth-plugin: getAttribute: " + name); return ""; } + @Override public Set getKeys() { log.info("crowd-phpbbauth-plugin: getAttributeNames"); @@ -112,11 +121,13 @@ public boolean hasAttribute(String name) return false; } + @Override public boolean isEmpty() { return true; } + @Override public User findUserByName(String name) throws UserNotFoundException { @@ -137,6 +148,15 @@ public User findUserByName(String name) } + @Override + public User findUserByExternalId(String externalId) + throws UserNotFoundException, OperationFailedException + { + log.info("crowd-phpbbauth-plugin: findUserByExternalId: " + externalId); + throw new OperationFailedException(); + } + + @Override public UserWithAttributes findUserWithAttributesByName(String name) throws UserNotFoundException, OperationFailedException { @@ -168,6 +188,7 @@ public UserWithAttributes findUserWithAttributesByName(String name) * @throws InvalidAuthenticationException Invalid username or password. * @throws ObjectNotFoundException Any other errors */ + @Override public User authenticate(String name, PasswordCredential credential) throws UserNotFoundException, InactiveAccountException, InvalidAuthenticationException, ExpiredCredentialException, OperationFailedException { @@ -189,11 +210,17 @@ public User authenticate(String name, PasswordCredential credential) if (!result.get(0).equals("success")) { + //NOTE: These messages will only appear in Crowd. Jira will ignore this and simply show "Sorry, your username and password are incorrect" String error = result.get(1); - if (error.equals("LOGIN_ERROR_ATTEMPTS") || error.equals("ACTIVE_ERROR")) + if (error.equals("LOGIN_ERROR_ATTEMPTS")) { - throw new InactiveAccountException(name); + throw new InvalidAuthenticationException("Too many failed logins on forum."); + } + + if (error.equals("ACTIVE_ERROR")) + { + throw new InactiveAccountException("Account is inactive."); } throw new InvalidAuthenticationException("Username or password are incorrect."); @@ -211,48 +238,62 @@ public User authenticate(String name, PasswordCredential credential) } } + @Override public User addUser(UserTemplate user, PasswordCredential credential) throws OperationFailedException { throw new OperationFailedException(); } + @Override + public UserWithAttributes addUser(UserTemplateWithAttributes user, PasswordCredential credential) + throws OperationFailedException { + throw new OperationFailedException(); + } + + @Override public User updateUser(UserTemplate user) throws OperationFailedException { throw new OperationFailedException(); } + @Override public void updateUserCredential(String username, PasswordCredential credential) throws OperationFailedException { throw new OperationFailedException(); } + @Override public User renameUser(String oldName, String newName) throws OperationFailedException, InvalidUserException { throw new OperationFailedException(); } + @Override public void storeUserAttributes(String username, Map> attributes) throws OperationFailedException { throw new OperationFailedException(); } + @Override public void removeUserAttributes(String username, String attributeName) throws OperationFailedException { throw new OperationFailedException(); } + @Override public void removeUser(String name) throws OperationFailedException { throw new OperationFailedException(); } + @Override public List searchUsers(EntityQuery query) { log.info("crowd-phpbbauth-plugin: searchUsers"); @@ -263,6 +304,7 @@ public List searchUsers(EntityQuery query) return list; } + @Override public Group findGroupByName(String name) throws GroupNotFoundException { @@ -282,6 +324,7 @@ public Group findGroupByName(String name) return (Group) list.get(0); } + @Override public GroupWithAttributes findGroupWithAttributesByName(String name) throws OperationFailedException, GroupNotFoundException { @@ -293,42 +336,49 @@ public GroupWithAttributes findGroupWithAttributesByName(String name) return (GroupWithAttributes) new GroupEntityCreator(getDirectoryId()).attachAttributes(group); } + @Override public Group addGroup(GroupTemplate group) throws OperationFailedException { throw new OperationFailedException(); } + @Override public Group updateGroup(GroupTemplate group) throws ReadOnlyGroupException { throw new ReadOnlyGroupException(group.getName()); } + @Override public Group renameGroup(String oldName, String newName) throws OperationFailedException { throw new OperationFailedException(); } + @Override public void storeGroupAttributes(String groupName, Map> attributes) throws OperationFailedException { throw new OperationFailedException(); } + @Override public void removeGroupAttributes(String groupName, String attributeName) throws OperationFailedException { throw new OperationFailedException(); } + @Override public void removeGroup(String name) throws ReadOnlyGroupException { throw new ReadOnlyGroupException(name); } + @Override public List searchGroups(EntityQuery query) { log.info("crowd-phpbbauth-plugin: searchGroups"); @@ -339,6 +389,7 @@ public List searchGroups(EntityQuery query) return list; } + @Override public boolean isUserDirectGroupMember(String username, String groupName) { log.info("crowd-phpbbauth-plugin: isUserDirectGroupMember: " + username + ", " + groupName); @@ -381,36 +432,49 @@ public boolean isUserDirectGroupMember(String username, String groupName) * * @return Always false. */ + @Override public boolean isGroupDirectGroupMember(String childGroup, String parentGroup) { log.info("crowd-phpbbauth-plugin: isGroupDirectGroupMember"); return false; } + @Override + public BoundedCount countDirectMembersOfGroup(String groupName, int querySizeHint) + throws OperationFailedException + { + throw new OperationFailedException(); + } + + @Override public void addUserToGroup(String username, String groupName) throws ReadOnlyGroupException { throw new ReadOnlyGroupException(groupName); } + @Override public void addGroupToGroup(String childGroup, String parentGroup) throws ReadOnlyGroupException { throw new ReadOnlyGroupException(parentGroup); } + @Override public void removeUserFromGroup(String username, String groupName) throws ReadOnlyGroupException { throw new ReadOnlyGroupException(groupName); } + @Override public void removeGroupFromGroup(String childGroup, String parentGroup) throws MembershipNotFoundException { throw new MembershipNotFoundException(childGroup, parentGroup); } + @Override public List searchGroupRelationships(MembershipQuery query) { log.info("crowd-phpbbauth-plugin: searchGroupRelationships"); @@ -423,7 +487,7 @@ public List searchGroupRelationships(MembershipQuery query) SearchRestriction searchRestriction = new TermRestriction( new PropertyImpl("name", String.class), MatchMode.EXACTLY_MATCHES, - query.getEntityNameToMatch() + query.getEntityNamesToMatch().iterator().next() // We only operate on single search terms, so grab the first ); if (query.getEntityToMatch().getEntityType() != Entity.GROUP) @@ -474,27 +538,50 @@ public List searchGroupRelationships(MembershipQuery query) return list; } + @Override public void testConnection() throws OperationFailedException { // could implement a simple http request here } + @Override + public void expireAllPasswords() + throws OperationFailedException + { + throw new OperationFailedException(); + } + /** * phpBB does not support nested Groups * * @return Always false. */ + @Override public boolean supportsNestedGroups() { return false; } + @Override + public boolean supportsPasswordExpiration() + { + return false; + } + + @Override + public boolean supportsSettingEncryptedCredential() + { + return false; + } + + @Override public RemoteDirectory getAuthoritativeDirectory() { return (RemoteDirectory) this; } + @Override public Iterable getMemberships() throws OperationFailedException { @@ -502,11 +589,13 @@ public Iterable getMemberships() throw new OperationFailedException(); } + @Override public boolean isRolesDisabled() { return true; } + @Override public boolean supportsInactiveAccounts() { return true; @@ -571,8 +660,13 @@ protected String restrictionToJson(SearchRestriction restriction) else if (restriction instanceof TermRestriction) { TermRestriction termRestriction = (TermRestriction) restriction; + String value = ""; + if (termRestriction.getValue() != null) + { + value = termRestriction.getValue().toString(); + } - return "{\"mode\": \"" + termRestriction.getMatchMode().toString() + "\", \"property\": \"" + escape(termRestriction.getProperty().getPropertyName()) + "\", \"value\": \"" + escape(termRestriction.getValue().toString()) + "\"}"; + return "{\"mode\": \"" + termRestriction.getMatchMode().toString() + "\", \"property\": \"" + escape(termRestriction.getProperty().getPropertyName()) + "\", \"value\": \"" + escape(value) + "\"}"; } else if (restriction instanceof BooleanRestrictionImpl) { @@ -633,8 +727,13 @@ protected ArrayList sendPostRequest(Map params) data += URLEncoder.encode(entry.getValue(), "UTF-8"); data += "&"; } + String api_url = serverUrl; + if (serverUrl.charAt(serverUrl.length() - 1) != '/') + { + api_url += "/"; + } - URL url = new URL(serverUrl + "auth_api.php"); + URL url = new URL(api_url + "auth_api.php"); URLConnection connection = url.openConnection(); connection.setDoOutput(true); @@ -662,4 +761,4 @@ protected ArrayList sendPostRequest(Map params) return result; } -} \ No newline at end of file +} diff --git a/crowd-phpbb-directory-server/src/main/resources/atlassian-plugin.xml b/crowd-phpbb-directory-server/src/main/resources/atlassian-plugin.xml new file mode 100644 index 0000000..5ecf7a2 --- /dev/null +++ b/crowd-phpbb-directory-server/src/main/resources/atlassian-plugin.xml @@ -0,0 +1,10 @@ + + + ${project.description} + ${project.version} + + + com.phpbb.crowd,*;resolution:=optional + + + \ No newline at end of file diff --git a/phpbb-external-auth-api/auth_api.php b/phpbb-external-auth-api/auth_api.php index b628c2a..9c46527 100644 --- a/phpbb-external-auth-api/auth_api.php +++ b/phpbb-external-auth-api/auth_api.php @@ -29,19 +29,16 @@ $phpbb_root_path = (defined('PHPBB_ROOT_PATH')) ? PHPBB_ROOT_PATH : './../community/'; $phpEx = substr(strrchr(__FILE__, '.'), 1); -// Report all errors, except notices and deprecation messages -if (!defined('E_DEPRECATED')) -{ - define('E_DEPRECATED', 8192); -} -error_reporting(E_ALL ^ E_NOTICE ^ E_DEPRECATED); +require($phpbb_root_path . 'includes/startup.' . $phpEx); +require($phpbb_root_path . 'phpbb/class_loader.' . $phpEx); + +$phpbb_class_loader = new \phpbb\class_loader('phpbb\\', "{$phpbb_root_path}phpbb/", $phpEx); +$phpbb_class_loader->register(); + +$phpbb_config_php_file = new \phpbb\config_php_file($phpbb_root_path, $phpEx); +extract($phpbb_config_php_file->get_all()); -if (version_compare(PHP_VERSION, '6.0.0-dev', '<')) -{ - @set_magic_quotes_runtime(0); -} -define('STRIP', (get_magic_quotes_gpc()) ? true : false); // Before we actually initialise all files, maybe we could simply return the important part quickly? if ($api_config['api_cache_users'] && !empty($_POST['action']) && $_POST['action'] == 'searchUsers') @@ -65,33 +62,50 @@ } // Include files -require($phpbb_root_path . 'includes/acm/acm_' . $acm_type . '.' . $phpEx); -require($phpbb_root_path . 'includes/cache.' . $phpEx); -require($phpbb_root_path . 'includes/db/' . $dbms . '.' . $phpEx); require($phpbb_root_path . 'includes/constants.' . $phpEx); require($phpbb_root_path . 'includes/utf/utf_tools.' . $phpEx); require($phpbb_root_path . 'includes/functions.' . $phpEx); require($phpbb_root_path . 'includes/functions_user.' . $phpEx); +include($phpbb_root_path . 'includes/functions_compatibility.' . $phpEx); -$db = new $sql_db(); -$cache = new cache(); +$phpbb_class_loader_ext = new \phpbb\class_loader('\\', "{$phpbb_root_path}ext/", $phpEx); +$phpbb_class_loader_ext->register(); -// Connect to DB -if (!@$db->sql_connect($dbhost, $dbuser, $dbpasswd, $dbname, $dbport, false, false)) +// Set up container +try { - exit; + $phpbb_container_builder = new \phpbb\di\container_builder($phpbb_root_path, $phpEx); + $phpbb_container = $phpbb_container_builder->with_config($phpbb_config_php_file)->get_container(); +} +catch (InvalidArgumentException $e) +{ + if (PHPBB_ENVIRONMENT !== 'development') + { + trigger_error( + 'The requested environment ' . PHPBB_ENVIRONMENT . ' is not available.', + E_USER_ERROR + ); + } + else + { + throw $e; + } } -unset($dbpasswd); -$config = $cache->obtain_config(); +$phpbb_class_loader->set_cache($phpbb_container->get('cache.driver')); +$phpbb_class_loader_ext->set_cache($phpbb_container->get('cache.driver')); + +$phpbb_container->get('dbal.conn')->set_debug_sql_explain($phpbb_container->getParameter('debug.sql_explain')); +$phpbb_container->get('dbal.conn')->set_debug_load_time($phpbb_container->getParameter('debug.load_time')); +require($phpbb_root_path . 'includes/compatibility_globals.' . $phpEx); + +register_compatibility_globals(); + +$user->session_begin(false); + +// Re-enable superglobals. This should be rewritten at some point to use the request system +$request->enable_super_globals(); -/* Try to get some custom "things" -$_POST['max'] = $_REQUEST['max'] = 1; -$_POST['restriction'] = $_REQUEST['restriction'] = "{\"mode\": \"EXACTLY_MATCHES\", \"property\": \"name\", \"value\": \"global moderators\"}"; -$_POST['start'] = $_REQUEST['start'] = 0; -$_POST['action'] = $_REQUEST['action'] = 'searchGroups'; -$_POST['returnType'] = $_REQUEST['returnType'] = 'ENTITY'; -*/ // Initialize auth API $api = new phpbb_auth_api($api_config); @@ -357,58 +371,16 @@ public function findUserByName() public function authenticate() { - global $db, $auth, $config, $phpEx, $phpbb_root_path; + global $db, $auth, $config, $phpbb_container; $username = request_var('name', '', true); $password = request_var('credential', '', true); - $err = ''; - // If authentication is successful we redirect user to previous page - //$method = trim(basename($config['auth_method'])); - // hardcode db auth method - $method = 'db'; - include_once($phpbb_root_path . 'includes/auth/auth_' . $method . '.' . $phpEx); - - $method = 'login_' . $method; - if (!function_exists($method)) - { - $this->add('error')->add('NO_AUTH'); - return; - } - - $result = $method($username, $password); - - // If the auth module wants us to create an empty profile do so and then treat the status as LOGIN_SUCCESS - if ($login['status'] == LOGIN_SUCCESS_CREATE_PROFILE) - { - // we are going to use the user_add function so include functions_user.php if it wasn't defined yet - if (!function_exists('user_add')) - { - include($phpbb_root_path . 'includes/functions_user.' . $phpEx); - } - - user_add($login['user_row'], (isset($login['cp_data'])) ? $login['cp_data'] : false); - - $sql = 'SELECT user_id, username, user_password, user_passchg, user_email, user_type - FROM ' . USERS_TABLE . " - WHERE username_clean = '" . $db->sql_escape(utf8_clean_string($username)) . "'"; - $result = $db->sql_query($sql); - $row = $db->sql_fetchrow($result); - $db->sql_freeresult($result); - - if (!$row) - { - $this->add('error')->add('Failed to create profile'); - return; - } - - $result = array( - 'status' => LOGIN_SUCCESS, - 'error_msg' => false, - 'user_row' => $row, - ); - } + /* @var $provider_collection \phpbb\auth\provider_collection */ + $provider_collection = $phpbb_container->get('auth.provider_collection'); + $provider = $provider_collection->get_provider(); + $result = $provider->login($username, $password); if (isset($result['user_row']['user_id'])) { @@ -429,9 +401,22 @@ public function authenticate() if ($result['status'] == LOGIN_SUCCESS) { // get avatar url - $sql = 'SELECT user_avatar, user_avatar_type, user_avatar_width, user_avatar_height - FROM ' . USERS_TABLE . ' - WHERE user_id = ' . $result['user_row']['user_id']; + $sql = 'SELECT u.user_avatar, u.user_avatar_type, u.user_avatar_width, u.user_avatar_height'; + + // get first/last name + if ($this->config['firstname_column'] && $this->config['lastname_column']) + { + $sql .= ', pf.pf_' . $this->config['firstname_column'] . ' as firstname, pf.pf_' . $this->config['lastname_column'] . ' as lastname'; + } + + $sql .= ' FROM ' . USERS_TABLE . ' u'; + + if ($this->config['firstname_column'] && $this->config['lastname_column']) + { + $sql .= ' LEFT JOIN ' . PROFILE_FIELDS_DATA_TABLE . ' pf ON (u.user_id = pf.user_id)'; + } + + $sql .= 'WHERE u.user_id = ' . $result['user_row']['user_id']; $sql_result = $db->sql_query($sql); $row = $db->sql_fetchrow($sql_result); $db->sql_freeresult($sql_result); @@ -441,6 +426,12 @@ public function authenticate() $result['user_row']['user_avatar_width'] = $row['user_avatar_width']; $result['user_row']['user_avatar_height'] = $row['user_avatar_height']; + if ($this->config['firstname_column'] && $this->config['lastname_column']) + { + $result['user_row']['firstname'] = $row['firstname']; + $result['user_row']['lastname'] = $row['lastname']; + } + $this->add('success')->add($this->user_row_line($result['user_row'])); return; } @@ -494,9 +485,6 @@ public function searchUsers() // Is it safe to assume our directory will only search for name? ;) array( 'email' => 'u.user_email', -/* 'lastName' => 'firstname', - 'firstName' => 'lastname', // always empty - 'displayName' => "CONCAT(firstname, ' ', lastname)",*/ 'name' => 'u.username_clean', 'active' => 'u.user_type' ));