diff --git a/groups.info b/groups.info index 2149f8b..3ebd944 100644 --- a/groups.info +++ b/groups.info @@ -152,6 +152,8 @@ dependencies[] = groups_footer dependencies[] = groups_homepage dependencies[] = groups_common dependencies[] = groups_auth +dependencies[] = groups_oauth2 +dependencies[] = groups_oauth2_picture dependencies[] = groups_feeds dependencies[] = groups_events dependencies[] = groups_events_pages diff --git a/groups.install b/groups.install index f95f59e..a5fe0aa 100644 --- a/groups.install +++ b/groups.install @@ -181,6 +181,16 @@ function groups_update_7107() { } } +/** + * Enable groups_oauth2 modules. + */ +function groups_update_7108() { + if ((!module_exists('groups_oauth2')) && (!module_exists('groups_oauth2_picture'))) { + module_enable(array('groups_oauth2', 'groups_oauth2_picture')); + drupal_flush_all_caches(); + } +} + /** * Add markdown filter with permissions. */ diff --git a/modules/groups/groups_auth/groups_auth.module b/modules/groups/groups_auth/groups_auth.module index 14f5e07..7a485b1 100644 --- a/modules/groups/groups_auth/groups_auth.module +++ b/modules/groups/groups_auth/groups_auth.module @@ -36,6 +36,10 @@ function groups_auth_menu_alter(&$items) { $items['user/%user/openid']['page callback'] = 'groups_auth_user_identities'; $items['user/%user/openid']['access callback'] = 'groups_auth_role_access'; $items['user/%user/openid']['access arguments'] = array('administrator'); + $items['user/%user/edit-profile']['access arguments'] = array('administrator'); + $items['user/%user/edit-profile']['access callback'] = 'groups_auth_role_access'; + $items['user/%user/edit']['access arguments'] = array('administrator'); + $items['user/%user/edit']['access callback'] = 'groups_auth_role_access'; } /** @@ -138,10 +142,14 @@ function groups_auth_form_user_register_form_alter(&$form, &$form_state) { */ function groups_auth_login() { - $return_to = url('openid/authenticate', - array('absolute' => TRUE, 'query' => array('destination' => ''))); - $openid_url = variable_get('groups_openid_provider', 'https://openstackid-dev.openstack.org'); - openid_begin($openid_url, $return_to, array()); + if (module_exists('groups_oauth2')) { + groups_oauth2_user_login_submit(); + } else { + $return_to = url('openid/authenticate', + array('absolute' => TRUE, 'query' => array('destination' => ''))); + $openid_url = variable_get('groups_openid_provider', 'https://openstackid-dev.openstack.org'); + openid_begin($openid_url, $return_to, array()); + } } /** @@ -177,6 +185,15 @@ function groups_auth_commons_utility_links_alter(&$links) { if (isset($links['signup'])) { $links['signup']['href'] = 'login'; } + if (isset($links['name'])) { + $links['name']['title'] = t('View profile'); + } + if (isset($links['logout'])) { + $links['logout']['weight'] = 1000; + } + if (isset($links['settings'])) { + unset($links['settings']); + } } /** diff --git a/modules/groups/groups_oauth2/README.txt b/modules/groups/groups_oauth2/README.txt new file mode 100644 index 0000000..51d5f41 --- /dev/null +++ b/modules/groups/groups_oauth2/README.txt @@ -0,0 +1,28 @@ +Description +----------- + +This module provides a basic oauth2 authentication for openstackid.org provider. Supports +fetching of first and family name, and fetch profile picture url. As openstackid provider +requires https communication, and the profile image assets not available through ssl, +the groups_auth2 module supports fetching of profile pictures into a local directory. + +Requirements +------------ +Drupal 7.x +Properly configured Oauth2 provider. + +Variables +--------- + +oauth2_fetch_profile_picture:boolean +If set to TRUE, downloads profile picture during login into the public://profile-images +directory. Default value is FALSE. + +groups_oauth2_provider:string +Contains the url of oauth2 provider. For openstackid, set it to https://openstackid.org + +groups_oauth2_client_id:string +The client id assigned for this specific application. + +groups_oauth2_client_secret:string +The client secret assigned for the client_id. \ No newline at end of file diff --git a/modules/groups/groups_oauth2/groups_oauth2.admin.inc b/modules/groups/groups_oauth2/groups_oauth2.admin.inc new file mode 100644 index 0000000..8187e67 --- /dev/null +++ b/modules/groups/groups_oauth2/groups_oauth2.admin.inc @@ -0,0 +1,40 @@ + 'textfield', + '#title' => t('OAuth2 Provider URL'), + '#default_value' => variable_get('groups_oauth2_provider'), + '#required' => TRUE, + ); + $form['groups_oauth2_client_id'] = array( + '#type' => 'textfield', + '#title' => t('OAuth2 Client ID'), + '#default_value' => variable_get('groups_oauth2_client_id'), + '#required' => TRUE, + ); + $form['groups_oauth2_client_secret'] = array( + '#type' => 'textfield', + '#title' => t('OAuth2 Client Secret'), + '#default_value' => variable_get('groups_oauth2_client_secret'), + '#required' => TRUE, + ); + $form['#validate'][] = 'groups_oauth2_admin_settings_validate'; + return system_settings_form($form); +} + +/** + * Form validation handler for groups_oauth2_admin_settings(). + */ +function groups_oauth2_admin_settings_validate($form, &$form_state) { + if (valid_url($form_state['values']['groups_oauth2_provider'], TRUE) == FALSE) { + form_set_error('allmyles_wsclient', t('The provider url value must be a valid url.')); + } +} \ No newline at end of file diff --git a/modules/groups/groups_oauth2/groups_oauth2.api.php b/modules/groups/groups_oauth2/groups_oauth2.api.php new file mode 100644 index 0000000..0e0434d --- /dev/null +++ b/modules/groups/groups_oauth2/groups_oauth2.api.php @@ -0,0 +1,24 @@ + 'groups_oauth2_response_handler', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + $items['admin/config/services/oauth2'] = array( + 'title' => 'Oauth2 Settings', + 'description' => 'OpenStackID Oauth2 settings', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('groups_oauth2_admin_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'groups_oauth2.admin.inc', + ); + return $items; +} + +/** + * Implements hook_form_alter() + * + * Add Login with OpenStackID button to login form. + */ +function groups_oauth2_form_user_login_alter(&$form, &$form_state, $form_id) { + if ($form_id == 'user_login' || $form_id == 'user_login_block') { + $form['submit_groups'] = array( + '#type' => 'submit', + '#value' => t('Login with OpenStackID'), + '#submit' => array('groups_oauth2_user_login_submit'), + '#limit_validation_errors' => array(), + '#weight' => 1000, + ); + } +} + +/** + * Handle login form submit. + * + * Redirect to oauth2 site and request an authorization code. + */ +function groups_oauth2_user_login_submit() { + if (!_groups_oauth2_validate_provider_settings()) { + return FALSE; + } + if (isset($_GET['destination'])) { + unset($_GET['destination']); + } + $redirect_uri = url('oauth2/response', array('https' => FALSE, 'absolute' => TRUE)); + $url = _groups_oauth2_authorize_url('code', $redirect_uri, 'auto', 'profile email'); + drupal_goto($url); +} + +/** + * Oauth2 response handler callback. + * + * Process to authorization code, invoke token and login or create + * a new user. + */ +function groups_oauth2_response_handler() { + if (isset($_GET['code'])) { + if (!_groups_oauth2_validate_provider_settings()) { + return FALSE; + } + $redirect_uri = url('oauth2/response', array('https' => $GLOBALS['is_https'], 'absolute' => TRUE)); + $response = _groups_oauth2_request_token('authorization_code', $_GET['code'], $redirect_uri); + if ($response->code == 200) { + $data = json_decode($response->data); + $userinfo = groups_oauth2_get_userinfo($data->access_token); + if (!$userinfo) { + drupal_set_message(t("Failed to retrieve user information"), 'error'); + drupal_goto(); + return; + } + $username = check_plain(strtolower($userinfo->name).'-'.strtolower($userinfo->family_name)); + $query = "SELECT uid FROM {users} WHERE name = :name"; + $uid = db_query($query, array(':name' => $username))->fetchField(); + $i = 0; + while ($uid) { + $i++; + $uid = db_query($query, array(':name' => ($username . $i)))->fetchField(); + } + if ($i > 0) { + $username = $username . $i; + } + $account = groups_oauth2_find_existing_user($userinfo); + if (!$account) { + $edit = array( + 'name' => $username, + 'mail' => $userinfo->email, + 'init' => $userinfo->email, + 'status' => 1, + 'timezone' => variable_get('date_default_timezone'), + 'data' => array( + 'oauth2_fullname' => check_plain($userinfo->name.' '.$userinfo->family_name), + ), + ); + $account = user_save(NULL, $edit); + // Allow other modules to manipulate the user information after save. + foreach (module_implements('oauth2_user_save') as $module) { + $function = $module . '_oauth2_user_save'; + $function($account, $userinfo); + } + } + $form_state['uid'] = $account->uid; + user_login_submit(array(), $form_state); + drupal_goto('user/' . $account->uid); + } else { + $data = json_decode($response->data); + drupal_set_message(t("The account is not authenticated. Reason: @error", + array('@error' => is_object($data) ? $data->error : 'Not known.')), 'error'); + } + } + drupal_goto(); +} + +/** + * Fetch user info from oauth2 server. + */ +function groups_oauth2_get_userinfo($access_token) { + $query = array('access_token' => $access_token); + $request_url = url(variable_get('groups_oauth2_provider').'/api/v1/users/me', array('query' => $query)); + $response = drupal_http_request($request_url); + if ($response->code == 200) { + return json_decode($response->data); + } + return FALSE; +} + +/** + * Find a Drupal user by oauth2 user info. + */ +function groups_oauth2_find_existing_user($userinfo) { + if ($user = user_load_by_mail($userinfo->email)) { + return $user; + } + return FALSE; +} + +/** + * Ensure groups oauth2 authentication variables. + * + * @return boolean + * Return TRUE if groups_oauth2_provider, groups_oauth2_client_id and + * groups_oauth2_client_secret are set, otherwise throw an error message. + */ +function _groups_oauth2_validate_provider_settings() { + $provider_url = variable_get('groups_oauth2_provider'); + $client_id = variable_get('groups_oauth2_client_id'); + $client_secret = variable_get('groups_oauth2_client_secret'); + if ((!isset($provider_url)) || (!isset($client_id)) || (!isset($client_secret))) { + drupal_set_message(t('Groups oauth2 provider not properly configured.'), 'warning'); + return FALSE; + } + return TRUE; +} + +/** + * Build oauth2 authorization url. + * + * @param string $response_type + * Oauth2 response type, use code for basic flow + * @param string $redirect_uri + * Determines where the response will be sent + * @param string $approval_prompt + * Approval prompt, set to auto + * @param string $scope + * Aouth2 scope requesting access for + * @return string + * The full url for authorization code request. + */ +function _groups_oauth2_authorize_url($response_type, $redirect_uri, $approval_prompt, $scope) { + $params = array( + 'response_type' => $response_type, + 'client_id' => variable_get('groups_oauth2_client_id'), + 'redirect_uri' => $redirect_uri, + 'approval_prompt' => $approval_prompt, + 'scope' => $scope, + ); + $provider_url = variable_get('groups_oauth2_provider'); + return $provider_url.'/oauth2/auth?'.http_build_query($params); +} + +/** + * Send a token request based on authorization code to oauth2 provider. + * + * @param string $grant_type + * Set to 'authorization_code' + * @param string $code + * Code received from authorization request. + * @param string $redirect_uri + * The redirect uri set for this application. + * @return [type] + * Http response of token request. + */ +function _groups_oauth2_request_token($grant_type, $code, $redirect_uri) { + $params = array( + 'grant_type' => $grant_type, + 'code' => $code, + 'redirect_uri' => $redirect_uri, + 'client_id' => variable_get('groups_oauth2_client_id'), + 'client_secret' => variable_get('groups_oauth2_client_secret'), + ); + $elements = array(); + foreach ($params as $k => $v) { + $elements[] = "$k=$v"; + } + $data = implode('&', $elements); + $options = array( + 'method' => 'POST', + 'data' => $data, + 'timeout' => 15, + 'headers' => array('Content-Type' => 'application/x-www-form-urlencoded'), + ); + $provider_url = variable_get('groups_oauth2_provider'); + return drupal_http_request($provider_url.'/oauth2/token', $options); +} + +/** + * Return the profile picture filename from url. + */ +function _groups_oauth2_get_profile_picture_filename($url) { + $parts = parse_url($url); + $basename = end(explode('/', $parts['path'])); + return 'public://profile-images/'.$basename; +} + +/** + * Implements hook_theme(). + */ +function groups_oauth2_theme() { + return array( + 'groups_oauth2_user_picture_override' => array( + 'variables' => array( + 'account' => NULL, + 'picture_url' => NULL, + ), + ), + ); +} + +/** + * Implements hook_theme_registry_alter(), override + * @param [type] $theme_registry [description] + * @return [type] [description] + */ +function groups_oauth2_theme_registry_alter(&$theme_registry) { + // Re-register the original theme function under a new name. + $theme_registry['groups_oauth2_user_picture_orig'] = $theme_registry['user_picture']; + // Override theme username + $theme_registry['user_picture'] = array( + 'arguments' => array('account' => NULL), + 'function' => 'groups_oauth2_theme_user_picture_override', + 'preprocess functions' => array(), + 'theme path' => drupal_get_path('module', 'groups_oauth2'), + ); +} + +/** + * Make rendering of Oauth2 user picture themeable + */ +function theme_groups_oauth2_user_picture_override($variables) { + $picture_url = $variables['picture_url']; + $account = $variables['account']; + $image = theme('image', array('path' => $picture_url, 'alt' => $account->name)); + return '
' . $image . '
'; +} + +/** + * Override user picture theme, and render oauth2 picture if available. + */ +function groups_oauth2_theme_user_picture_override($variables) { + $account = $variables['account']; + if (isset($account->data['oauth2_picture'])) { + if (variable_get('oauth2_fetch_profile_picture', FALSE)) { + $image_path = _groups_oauth2_get_profile_picture_filename($account->data['oauth2_picture']); + $picture_url = file_create_url($image_path); + } else { + $picture_url = $account->data['oauth2_picture']; + } + $output = theme('groups_oauth2_user_picture_override', array('account' => $account, 'picture_url' => $picture_url)); + } else { + $output = theme('groups_oauth2_user_picture_orig', array('account' => $account)); + } + return $output; +} + +/** + * Implements hook_username_alter(). + * + * @see realname_username_alter() + */ +function groups_oauth2_username_alter(&$name, $account) { + static $in_username_alter = FALSE; + if (empty($account->uid)) { + // Don't alter anonymous users or objects that do not have any user ID. + return; + } + // Name was loaded/generated via hook_user_load(), so re-use it. + if (isset($account->data['oauth2_fullname'])) { + if (drupal_strlen($account->data['oauth2_fullname'])) { + $name = $account->data['oauth2_fullname']; + } + return; + } + // Real name was not yet available for the account so we need to generate it. + // Because tokens may call format_username() we need to prevent recursion. + if (!$in_username_alter) { + $in_username_alter = TRUE; + // If $account->realname was undefined, then the user account object was + // not properly loaded. We must enforce calling user_load(). + if ($oauth2_account = user_load($account->uid)) { + groups_oauth2_username_alter($name, $oauth2_account); + } + $in_username_alter = FALSE; + } +} \ No newline at end of file diff --git a/modules/groups/groups_oauth2/groups_oauth2_picture/groups_oauth2_picture.info b/modules/groups/groups_oauth2/groups_oauth2_picture/groups_oauth2_picture.info new file mode 100644 index 0000000..6c73e0b --- /dev/null +++ b/modules/groups/groups_oauth2/groups_oauth2_picture/groups_oauth2_picture.info @@ -0,0 +1,7 @@ +name = Groups OAuth2 Profile Picture +description = "Profile Picture support for Groups OAuth2 authentication" +core = 7.x +package = Groups - Authentication + +version = "7.x-1.0" +project = "groups" \ No newline at end of file diff --git a/modules/groups/groups_oauth2/groups_oauth2_picture/groups_oauth2_picture.install b/modules/groups/groups_oauth2/groups_oauth2_picture/groups_oauth2_picture.install new file mode 100644 index 0000000..dc58867 --- /dev/null +++ b/modules/groups/groups_oauth2/groups_oauth2_picture/groups_oauth2_picture.install @@ -0,0 +1,13 @@ +picture); + $picture_result = drupal_http_request($userinfo->picture); + $picture_file = file_save_data($picture_result->data, $image_path, FILE_EXISTS_REPLACE); + $image_info = image_get_info($image_path); + $file = new StdClass(); + $validators = array( + 'file_validate_is_image' => array(), + ); + $errors = file_validate($picture_file, $validators); + if (empty($errors)) { + $picture_file->uid = $account->uid; + $picture_file = file_save($picture_file); + file_usage_add($picture_file, 'user', 'user', $account->uid); + db_update('users') + ->fields(array( + 'picture' => $picture_file->fid, + )) + ->condition('uid', $account->uid) + ->execute(); + } + } + $account->data['oauth2_picture'] = $userinfo->picture; + user_save($account); +} \ No newline at end of file diff --git a/themes/openstack_bootstrap/template.php b/themes/openstack_bootstrap/template.php index c45b5f8..0a49b2d 100755 --- a/themes/openstack_bootstrap/template.php +++ b/themes/openstack_bootstrap/template.php @@ -49,7 +49,7 @@ function openstack_bootstrap_links__commons_utility_links(&$variables) { $variables['attributes']['class'][] = 'dropdown-menu-right'; $output = '
'; $output .= ''; $output .= theme_links($variables); $output .= '
';