Merge branch 'MDL-70689-311' of git://github.com/sarjona/moodle into MOODLE_311_STABLE

This commit is contained in:
Eloy Lafuente (stronk7)
2021-03-17 23:20:50 +01:00
17 changed files with 1293 additions and 195 deletions
+22 -4
View File
@@ -103,13 +103,11 @@ class issuer extends persistent {
// Client ID.
$mform->addElement('text', 'clientid', get_string('issuerclientid', 'tool_oauth2'));
$mform->addRule('clientid', null, 'required', null, 'client');
$mform->addRule('clientid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
$mform->addHelpButton('clientid', 'issuerclientid', 'tool_oauth2');
// Client Secret.
$mform->addElement('text', 'clientsecret', get_string('issuerclientsecret', 'tool_oauth2'));
$mform->addRule('clientsecret', null, 'required', null, 'client');
$mform->addRule('clientsecret', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
$mform->addHelpButton('clientsecret', 'issuerclientsecret', 'tool_oauth2');
@@ -167,15 +165,25 @@ class issuer extends persistent {
$mform->addHelpButton('requireconfirmation', 'issuerrequireconfirmation', 'tool_oauth2');
}
if ($this->type == 'imsobv2p1' || $issuer->get('servicetype') == 'imsobv2p1') {
$mform->addRule('baseurl', null, 'required', null, 'client');
} else {
$mform->addRule('clientid', null, 'required', null, 'client');
$mform->addRule('clientsecret', null, 'required', null, 'client');
}
$mform->addElement('hidden', 'sortorder');
$mform->setType('sortorder', PARAM_INT);
$mform->addElement('hidden', 'servicetype');
$mform->setType('servicetype', PARAM_ALPHANUM);
if ($this->type) {
$mform->addElement('hidden', 'action', 'savetemplate');
$mform->setType('action', PARAM_ALPHA);
$mform->addElement('hidden', 'type', $this->_customdata['type']);
$mform->setType('type', PARAM_ALPHA);
$mform->setType('type', PARAM_ALPHANUM);
} else {
$mform->addElement('hidden', 'action', 'edit');
$mform->setType('action', PARAM_ALPHA);
@@ -190,5 +198,15 @@ class issuer extends persistent {
$this->add_action_buttons(true, get_string('savechanges', 'tool_oauth2'));
}
}
/**
* This method implements changes to the form that need to be made once the form data is set.
*/
public function definition_after_data() {
$mform = $this->_form;
if ($this->type) {
// Set servicetype if it's defined.
$mform->getElement('servicetype')->setValue($this->type);
}
}
}
+23 -9
View File
@@ -53,7 +53,7 @@ if ($action == 'edit') {
if ($issuer) {
$PAGE->navbar->add(get_string('editissuer', 'tool_oauth2', s($issuer->get('name'))));
} else {
$PAGE->navbar->add(get_string('createnewissuer', 'tool_oauth2'));
$PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . get_string('custom_service', 'tool_oauth2'));
}
$showrequireconfirm = false;
@@ -96,14 +96,14 @@ if ($mform && $mform->is_cancelled()) {
if ($issuer) {
echo $OUTPUT->heading(get_string('editissuer', 'tool_oauth2', s($issuer->get('name'))));
} else {
echo $OUTPUT->heading(get_string('createnewissuer', 'tool_oauth2'));
echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string('custom_service', 'tool_oauth2'));
}
$mform->display();
echo $OUTPUT->footer();
}
} else if ($action == 'savetemplate') {
$type = required_param('type', PARAM_ALPHA);
$type = required_param('type', PARAM_ALPHANUM);
$mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer, 'type' => $type]);
if ($mform->is_cancelled()) {
redirect(new moodle_url('/admin/tool/oauth2/issuers.php'));
@@ -115,13 +115,14 @@ if ($mform && $mform->is_cancelled()) {
redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
} else {
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
$mform->display();
echo $OUTPUT->footer();
}
} else if ($action == 'edittemplate') {
$type = required_param('type', PARAM_ALPHA);
$type = required_param('type', PARAM_ALPHANUM);
$docs = required_param('docslink', PARAM_ALPHAEXT);
$showrequireconfirm = optional_param('showrequireconfirm', false, PARAM_BOOL);
require_sesskey();
@@ -129,7 +130,9 @@ if ($mform && $mform->is_cancelled()) {
$mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer, 'type' => $type,
'showrequireconfirm' => $showrequireconfirm]);
$PAGE->navbar->add(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('createnewservice', 'tool_oauth2') . get_string($type . '_service', 'tool_oauth2'));
$mform->display();
echo $OUTPUT->footer();
@@ -195,36 +198,47 @@ if ($mform && $mform->is_cancelled()) {
$issuers = core\oauth2\api::get_all_issuers();
echo $renderer->issuers_table($issuers);
echo $renderer->container_start();
echo get_string('createnewservice', 'tool_oauth2');
// Google template.
$docs = 'admin/tool/oauth2/issuers/google';
$params = ['action' => 'edittemplate', 'type' => 'google', 'sesskey' => sesskey(), 'docslink' => $docs,
'showrequireconfirm' => true];
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
echo $renderer->single_button($addurl, get_string('createnewgoogleissuer', 'tool_oauth2'));
echo $renderer->single_button($addurl, get_string('google_service', 'tool_oauth2'));
// Microsoft template.
$docs = 'admin/tool/oauth2/issuers/microsoft';
$params = ['action' => 'edittemplate', 'type' => 'microsoft', 'sesskey' => sesskey(), 'docslink' => $docs,
'showrequireconfirm' => true];
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
echo $renderer->single_button($addurl, get_string('createnewmicrosoftissuer', 'tool_oauth2'));
echo $renderer->single_button($addurl, get_string('microsoft_service', 'tool_oauth2'));
// Facebook template.
$docs = 'admin/tool/oauth2/issuers/facebook';
$params = ['action' => 'edittemplate', 'type' => 'facebook', 'sesskey' => sesskey(), 'docslink' => $docs,
'showrequireconfirm' => true];
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
echo $renderer->single_button($addurl, get_string('createnewfacebookissuer', 'tool_oauth2'));
echo $renderer->single_button($addurl, get_string('facebook_service', 'tool_oauth2'));
// Nextcloud template.
$docs = 'admin/tool/oauth2/issuers/nextcloud';
$params = ['action' => 'edittemplate', 'type' => 'nextcloud', 'sesskey' => sesskey(), 'docslink' => $docs];
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
echo $renderer->single_button($addurl, get_string('createnewnextcloudissuer', 'tool_oauth2'));
echo $renderer->single_button($addurl, get_string('nextcloud_service', 'tool_oauth2'));
// IMS Open Badges Connect template.
$docs = 'admin/tool/oauth2/issuers/imsobv2p1';
$params = ['action' => 'edittemplate', 'type' => 'imsobv2p1', 'sesskey' => sesskey(), 'docslink' => $docs];
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params);
echo $renderer->single_button($addurl, get_string('imsobv2p1_service', 'tool_oauth2'));
// Generic issuer.
$addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']);
echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2'));
echo $renderer->single_button($addurl, get_string('custom_service', 'tool_oauth2'));
echo $renderer->container_end();
echo $OUTPUT->footer();
+5
View File
@@ -0,0 +1,5 @@
createnewfacebookissuer,tool_oauth2
createnewgoogleissuer,tool_oauth2
createnewissuer,tool_oauth2
createnewmicrosoftissuer,tool_oauth2
createnewnextcloudissuer,tool_oauth2
+14 -5
View File
@@ -31,12 +31,9 @@ $string['connectsystemaccount'] = 'Connect to a system account';
$string['createfromtemplate'] = 'Create an OAuth 2 service from a template';
$string['createfromtemplatedesc'] = 'Choose one of the OAuth 2 service templates below to create an OAuth service with a valid configuration for one of the known service types. This will create the OAuth 2 service, with all the correct end points and parameters required for authentication, though you will still need to enter the client ID and secret for the new service before it can be used.';
$string['createnewendpoint'] = 'Create new endpoint for issuer "{$a}"';
$string['createnewfacebookissuer'] = 'Create new Facebook service';
$string['createnewgoogleissuer'] = 'Create new Google service';
$string['createnewissuer'] = 'Create new custom service';
$string['createnewmicrosoftissuer'] = 'Create new Microsoft service';
$string['createnewnextcloudissuer'] = 'Create new Nextcloud service';
$string['createnewservice'] = 'Create new service: ';
$string['createnewuserfieldmapping'] = 'Create new user field mapping for issuer "{$a}"';
$string['custom_service'] = 'Custom';
$string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.';
$string['deleteendpointconfirm'] = 'Are you sure you want to delete the endpoint "{$a->endpoint}" for issuer "{$a->issuer}"? Any plugins relying on this endpoint will stop working.';
$string['deleteuserfieldmappingconfirm'] = 'Are you sure you want to delete the user field mapping for issuer "{$a}"?';
@@ -54,6 +51,9 @@ $string['endpointname'] = 'Name';
$string['endpointsforissuer'] = 'Endpoints for issuer: {$a}';
$string['endpointurl_help'] = 'URL for this endpoint. Must use https:// protocol.';
$string['endpointurl'] = 'URL';
$string['facebook_service'] = 'Facebook';
$string['google_service'] = 'Google';
$string['imsobv2p1_service'] = 'IMS OBv2.1';
$string['issuersetup'] = 'Detailed instructions on configuring the common OAuth 2 services';
$string['issuersetuptype'] = 'Detailed instructions on setting up the {$a} OAuth 2 provider';
$string['issueralloweddomains_help'] = 'If set, this setting is a comma separated list of domains that logins will be restricted to when using this provider.';
@@ -86,6 +86,8 @@ $string['issuerrequireconfirmation_help'] = 'Require that all users verify their
$string['issuerrequireconfirmation'] = 'Require email verification';
$string['issuers'] = 'Issuers';
$string['loginissuer'] = 'Allow login';
$string['microsoft_service'] = 'Microsoft';
$string['nextcloud_service'] = 'Nextcloud';
$string['notconfigured'] = 'Not configured';
$string['notdiscovered'] = 'Service discovery not successful';
$string['notloginissuer'] = 'Do not allow login';
@@ -106,3 +108,10 @@ $string['userfieldinternalfield'] = 'Internal field name';
$string['userfieldmappingdeleted'] = 'User field mapping deleted';
$string['userfieldmappingsforissuer'] = 'User field mappings for issuer: {$a}';
$string['privacy:metadata'] = 'The OAuth 2 services plugin does not store any personal data.';
// Deprecated since Moodle 3.11.
$string['createnewfacebookissuer'] = 'Create new Facebook service';
$string['createnewgoogleissuer'] = 'Create new Google service';
$string['createnewissuer'] = 'Create new custom service';
$string['createnewmicrosoftissuer'] = 'Create new Microsoft service';
$string['createnewnextcloudissuer'] = 'Create new Nextcloud service';
@@ -0,0 +1,303 @@
@tool @tool_oauth2 @external
Feature: Basic OAuth2 functionality
In order to use them later for authentication or repository plugins
As an administrator
I need to add a manage customised OAuth2 services.
Background:
Given I log in as "admin"
And I navigate to "Server > OAuth 2 services" in site administration
Scenario: Create, edit and delete standard service for Google
Given I press "Google"
And I should see "Create new service: Google"
And I set the following fields to these values:
| Name | Testing service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
When I press "Save changes"
Then I should see "Changes saved"
And I should see "Testing service"
And "Configured" "icon" should exist in the "Testing service" "table_row"
And "Allow login" "icon" should exist in the "Testing service" "table_row"
And "Service discovery successful" "icon" should exist in the "Testing service" "table_row"
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "given_name" "table_row"
And I should see "middlename" in the "middle_name" "table_row"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
And I press "Save changes"
And I should see "Changes saved"
And I should see "Testing service modified"
And I click on "Delete" "link" in the "Testing service modified" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"Testing service modified\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "Testing service modified"
Scenario: Create, edit and delete standard service for Microsoft
Given I press "Microsoft"
And I should see "Create new service: Microsoft"
And I set the following fields to these values:
| Name | Testing service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
When I press "Save changes"
Then I should see "Changes saved"
And I should see "Testing service"
And "Configured" "icon" should exist in the "Testing service" "table_row"
And "Allow login" "icon" should exist in the "Testing service" "table_row"
And I should see "-" in the "Testing service" "table_row"
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "givenName" "table_row"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
And I press "Save changes"
And I should see "Changes saved"
And I should see "Testing service modified"
And I click on "Delete" "link" in the "Testing service modified" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"Testing service modified\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "Testing service modified"
Scenario: Create, edit and delete standard service for Facebook
Given I press "Facebook"
And I should see "Create new service: Facebook"
And I set the following fields to these values:
| Name | Testing service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
When I press "Save changes"
Then I should see "Changes saved"
And I should see "Testing service"
And "Configured" "icon" should exist in the "Testing service" "table_row"
And "Allow login" "icon" should exist in the "Testing service" "table_row"
And I should see "-" in the "Testing service" "table_row"
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "first_name" "table_row"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
And I press "Save changes"
And I should see "Changes saved"
And I should see "Testing service modified"
And I click on "Delete" "link" in the "Testing service modified" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"Testing service modified\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "Testing service modified"
@javascript
Scenario: Create, edit and delete standard service for Nextcloud
Given I press "Nextcloud"
And I should see "Create new service: Nextcloud"
And I set the following fields to these values:
| Name | Testing service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
And I press "Save changes"
And I should see "You must supply a value here."
And I set the following fields to these values:
| Service base URL | https://dummy.local/nextcloud/ |
When I press "Save changes"
Then I should see "Changes saved"
And I should see "Testing service"
And "Configured" "icon" should exist in the "Testing service" "table_row"
And "Do not allow login" "icon" should exist in the "Testing service" "table_row"
And I should see "-" in the "Testing service" "table_row"
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "username" in the "ocs-data-id" "table_row"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
And I press "Save changes"
And I should see "Could not discover service endpoints"
And I should see "Testing service modified"
And I click on "Delete" "link" in the "Testing service modified" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"Testing service modified\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "Testing service modified"
Scenario: Create, edit and delete standard service for IMS OBv2.1
Given I press "IMS OBv2.1"
And I should see "Create new service: IMS OBv2.1"
And I set the following fields to these values:
| Client ID | thisistheclientid |
| Client secret | supersecret |
| Service base URL | https://dc.imsglobal.org/ |
When I press "Save changes"
Then I should see "Changes saved"
And I should see "IMS OBv2.1"
And "Configured" "icon" should exist in the "IMS OBv2.1" "table_row"
And "Do not allow login" "icon" should exist in the "IMS OBv2.1" "table_row"
And "Service discovery successful" "icon" should exist in the "IMS OBv2.1" "table_row"
And the "src" attribute of "table.admintable th img" "css_element" should contain "IMS-Global-Logo.png"
And I click on "Configure endpoints" "link" in the "IMS OBv2.1" "table_row"
And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "IMS OBv2.1" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "IMS OBv2.1" "table_row"
And I set the following fields to these values:
| Name | IMS Global |
And I press "Save changes"
And I should see "Changes saved"
And I should see "IMS Global"
And I click on "Delete" "link" in the "IMS Global" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"IMS Global\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "IMS Global"
Scenario: Create, edit and delete valid custom OIDC service
Given I press "Custom"
And I should see "Create new service: Custom"
And I set the following fields to these values:
| Name | Google custom |
| Client ID | thisistheclientid |
| Client secret | supersecret |
| Service base URL | https://accounts.google.com/ |
When I press "Save changes"
Then I should see "Changes saved"
And I should see "Google custom"
And "Configured" "icon" should exist in the "Google custom" "table_row"
And "Do not allow login" "icon" should exist in the "Google custom" "table_row"
And "Service discovery successful" "icon" should exist in the "Google custom" "table_row"
And the "src" attribute of "table.admintable th img" "css_element" should contain "favicon.ico"
And I click on "Configure endpoints" "link" in the "Google custom" "table_row"
And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "Google custom" "table_row"
And I should see "firstname" in the "given_name" "table_row"
And I should see "middlename" in the "middle_name" "table_row"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "Google custom" "table_row"
And I set the following fields to these values:
| Name | Google custom modified |
And I press "Save changes"
And I should see "Changes saved"
And I should see "Google custom modified"
And I click on "Delete" "link" in the "Google custom modified" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"Google custom modified\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "Google custom modified"
Scenario: Create, edit and delete invalid custom OIDC service
Given I press "Custom"
And I should see "Create new service: Custom"
And I set the following fields to these values:
| Name | Invalid custom service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
| Service base URL | https://dc.imsglobal.org/ |
When I press "Save changes"
Then I should see "Could not discover end points for identity issuer: Invalid custom service"
And I should see "URL: https://dc.imsglobal.org/.well-known/openid-configuration"
And "Configured" "icon" should exist in the "Invalid custom service" "table_row"
And "Do not allow login" "icon" should exist in the "Invalid custom service" "table_row"
And I should see "-" in the "Invalid custom service" "table_row"
And I click on "Configure endpoints" "link" in the "Invalid custom service" "table_row"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "Invalid custom service" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "Invalid custom service" "table_row"
And I set the following fields to these values:
| Name | Valid custom service |
| Service base URL | https://accounts.google.com/ |
And I press "Save changes"
And "Configured" "icon" should exist in the "Valid custom" "table_row"
And "Do not allow login" "icon" should exist in the "Valid custom" "table_row"
And "Service discovery successful" "icon" should exist in the "Valid custom" "table_row"
And I click on "Edit" "link" in the "Valid custom service" "table_row"
And I set the following fields to these values:
| Name | Invalid custom service |
| Service base URL | https://dc.imsglobal.org/ |
And I press "Save changes"
Then I should see "Could not discover end points for identity issuer: Invalid custom service"
And I should see "-" in the "Invalid custom service" "table_row"
And I click on "Delete" "link" in the "Invalid custom service" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"Invalid custom service\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "Invalid custom service"
Scenario: Create, edit and delete empty custom OIDC service
Given I press "Custom"
And I should see "Create new service: Custom"
And I set the following fields to these values:
| Name | Empty custom service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
When I press "Save changes"
And I should see "Changes saved"
And I should see "Empty custom service"
And "Configured" "icon" should exist in the "Empty custom service" "table_row"
And "Do not allow login" "icon" should exist in the "Empty custom service" "table_row"
And I should see "-" in the "Empty custom service" "table_row"
And I click on "Configure endpoints" "link" in the "Empty custom service" "table_row"
And I should not see "discovery_endpoint"
And I follow "OAuth 2 services"
And I click on "Configure user field mappings" "link" in the "Empty custom service" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
And I follow "OAuth 2 services"
And I click on "Edit" "link" in the "Empty custom service" "table_row"
# Check it works as expected too without slash at the end of the service base URL.
And I set the following fields to these values:
| Name | Valid custom service |
| Service base URL | https://accounts.google.com |
And I press "Save changes"
And "Configured" "icon" should exist in the "Valid custom" "table_row"
And "Do not allow login" "icon" should exist in the "Valid custom" "table_row"
And "Service discovery successful" "icon" should exist in the "Valid custom" "table_row"
And I click on "Edit" "link" in the "Valid custom service" "table_row"
And I set the following fields to these values:
| Name | Invalid custom service |
| Service base URL | https://dc.imsglobal.org/ |
And I press "Save changes"
Then I should see "Could not discover end points for identity issuer: Invalid custom service"
And I should see "-" in the "Invalid custom service" "table_row"
And I click on "Edit" "link" in the "Invalid custom service" "table_row"
And I set the following fields to these values:
| Name | Empty custom service |
| Service base URL | |
And I press "Save changes"
And I should see "Changes saved"
And I should see "Empty custom service"
And I click on "Delete" "link" in the "Empty custom service" "table_row"
And I should see "Are you sure you want to delete the identity issuer \"Empty custom service\"?"
And I press "Continue"
And I should see "Identity issuer deleted"
And I should not see "Empty custom service"
+105 -175
View File
@@ -27,12 +27,10 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/filelib.php');
use context_system;
use curl;
use stdClass;
use moodle_exception;
use moodle_url;
use context_system;
use moodle_exception;
/**
* Static list of api methods for system oauth2 configuration.
@@ -42,42 +40,6 @@ use moodle_url;
*/
class api {
/**
* Build a google ready OAuth 2 service.
* @return \core\oauth2\issuer
*/
private static function init_google() {
$record = (object) [
'name' => 'Google',
'image' => 'https://accounts.google.com/favicon.ico',
'baseurl' => 'https://accounts.google.com/',
'loginparamsoffline' => 'access_type=offline&prompt=consent',
'showonloginpage' => true
];
$issuer = new issuer(0, $record);
return $issuer;
}
/**
* Create endpoints for google issuers.
* @param issuer $issuer issuer the endpoints should be created for.
* @return mixed
* @throws \coding_exception
* @throws \core\invalid_persistent_exception
*/
private static function create_endpoints_for_google($issuer) {
$record = (object) [
'issuerid' => $issuer->get('id'),
'name' => 'discovery_endpoint',
'url' => 'https://accounts.google.com/.well-known/openid-configuration'
];
$endpoint = new endpoint(0, $record);
$endpoint->create();
return $issuer;
}
/**
* Build a facebook ready OAuth 2 service.
* @return \core\oauth2\issuer
@@ -90,7 +52,8 @@ class api {
'baseurl' => '',
'loginscopes' => 'public_profile email',
'loginscopesoffline' => 'public_profile email',
'showonloginpage' => true
'showonloginpage' => true,
'servicetype' => 'facebook',
];
$issuer = new issuer(0, $record);
@@ -168,7 +131,8 @@ class api {
'baseurl' => '',
'loginscopes' => 'openid profile email user.read',
'loginscopesoffline' => 'openid profile email user.read offline_access',
'showonloginpage' => true
'showonloginpage' => true,
'servicetype' => 'microsoft',
];
$issuer = new issuer(0, $record);
@@ -233,6 +197,7 @@ class api {
'name' => 'Nextcloud',
'image' => 'https://nextcloud.com/wp-content/themes/next/assets/img/common/favicon.png?x16328',
'basicauth' => 1,
'servicetype' => 'nextcloud',
];
$issuer = new issuer(0, $record);
@@ -287,83 +252,105 @@ class api {
$userfieldmapping = new \core\oauth2\user_field_mapping(0, $record);
$userfieldmapping->create();
}
return $issuer;
}
/**
* Initializes a record for one of the standard issuers to be displayed in the settings.
* The issuer is not yet created in the database.
* @param string $type One of google, facebook, microsoft, nextcloud
* @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1
* @return \core\oauth2\issuer
*/
public static function init_standard_issuer($type) {
require_capability('moodle/site:config', context_system::instance());
if ($type == 'google') {
return self::init_google();
} else if ($type == 'microsoft') {
// TODO: Move these methods to new service classes (to make this API easier to understand and maintain).
if ($type == 'microsoft') {
return self::init_microsoft();
} else if ($type == 'facebook') {
return self::init_facebook();
} else if ($type == 'nextcloud') {
return self::init_nextcloud();
} else {
$classname = self::get_service_classname($type);
if (class_exists($classname)) {
return $classname::init();
}
throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
}
}
/**
* Create endpoints for standard issuers, based on the issuer created from submitted data.
* @param string $type One of google, facebook, microsoft, nextcloud
* @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1
* @param issuer $issuer issuer the endpoints should be created for.
* @return \core\oauth2\issuer
*/
public static function create_endpoints_for_standard_issuer($type, $issuer) {
require_capability('moodle/site:config', context_system::instance());
if ($type == 'google') {
$issuer = self::create_endpoints_for_google($issuer);
self::discover_endpoints($issuer);
return $issuer;
} else if ($type == 'microsoft') {
// TODO: Move these methods to new service classes (to make this API easier to understand and maintain).
if ($type == 'microsoft') {
return self::create_endpoints_for_microsoft($issuer);
} else if ($type == 'facebook') {
return self::create_endpoints_for_facebook($issuer);
} else if ($type == 'nextcloud') {
return self::create_endpoints_for_nextcloud($issuer);
} else {
$classname = self::get_service_classname($type);
if (class_exists($classname)) {
$classname::create_endpoints($issuer);
return $issuer;
}
throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
}
}
/**
* Create one of the standard issuers.
* @param string $type One of google, facebook, microsoft, or nextcloud
* @param string|false $baseurl Baseurl (only required for nextcloud)
*
* @param string $type One of google, facebook, microsoft, nextcloud or imsobv2p1
* @param string|false $baseurl Baseurl (only required for nextcloud and imsobv2p1)
* @return \core\oauth2\issuer
*/
public static function create_standard_issuer($type, $baseurl = false) {
require_capability('moodle/site:config', context_system::instance());
if ($type == 'google') {
$issuer = self::init_google();
$issuer->create();
return self::create_endpoints_for_google($issuer);
} else if ($type == 'microsoft') {
$issuer = self::init_microsoft();
$issuer->create();
return self::create_endpoints_for_microsoft($issuer);
} else if ($type == 'facebook') {
$issuer = self::init_facebook();
$issuer->create();
return self::create_endpoints_for_facebook($issuer);
} else if ($type == 'nextcloud') {
if (!$baseurl) {
throw new moodle_exception('Nextcloud service type requires the baseurl parameter.');
}
$issuer = self::init_nextcloud();
$issuer->set('baseurl', $baseurl);
$issuer->create();
return self::create_endpoints_for_nextcloud($issuer);
} else {
throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
switch ($type) {
case 'imsobv2p1':
if (!$baseurl) {
throw new moodle_exception('IMS OBv2.1 service type requires the baseurl parameter.');
}
case 'google':
$classname = self::get_service_classname($type);
$issuer = $classname::init();
if ($baseurl) {
$issuer->set('baseurl', $baseurl);
}
$issuer->create();
return self::create_endpoints_for_standard_issuer($type, $issuer);
case 'microsoft':
$issuer = self::init_microsoft();
$issuer->create();
return self::create_endpoints_for_microsoft($issuer);
case 'facebook':
$issuer = self::init_facebook();
$issuer->create();
return self::create_endpoints_for_facebook($issuer);
case 'nextcloud':
if (!$baseurl) {
throw new moodle_exception('Nextcloud service type requires the baseurl parameter.');
}
$issuer = self::init_nextcloud();
$issuer->set('baseurl', $baseurl);
$issuer->create();
return self::create_endpoints_for_nextcloud($issuer);
}
throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
}
@@ -528,93 +515,6 @@ class api {
}
}
/**
* If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints.
*
* @param issuer $issuer
* @return int The number of discovered services.
*/
protected static function discover_endpoints($issuer) {
$curl = new curl();
if (empty($issuer->get('baseurl'))) {
return 0;
}
$url = $issuer->get_endpoint_url('discovery');
if (!$url) {
$url = $issuer->get('baseurl') . '/.well-known/openid-configuration';
}
if (!$json = $curl->get($url)) {
$msg = 'Could not discover end points for identity issuer' . $issuer->get('name');
throw new moodle_exception($msg);
}
if ($msg = $curl->error) {
throw new moodle_exception('Could not discover service endpoints: ' . $msg);
}
$info = json_decode($json);
if (empty($info)) {
$msg = 'Could not discover end points for identity issuer' . $issuer->get('name');
throw new moodle_exception($msg);
}
foreach (endpoint::get_records(['issuerid' => $issuer->get('id')]) as $endpoint) {
if ($endpoint->get('name') != 'discovery_endpoint') {
$endpoint->delete();
}
}
foreach ($info as $key => $value) {
if (substr_compare($key, '_endpoint', - strlen('_endpoint')) === 0) {
$record = new stdClass();
$record->issuerid = $issuer->get('id');
$record->name = $key;
$record->url = $value;
$endpoint = new endpoint(0, $record);
$endpoint->create();
}
if ($key == 'scopes_supported') {
$issuer->set('scopessupported', implode(' ', $value));
$issuer->update();
}
}
// We got to here - must be a decent OpenID connect service. Add the default user field mapping list.
foreach (user_field_mapping::get_records(['issuerid' => $issuer->get('id')]) as $userfieldmapping) {
$userfieldmapping->delete();
}
// Create the field mappings.
$mapping = [
'given_name' => 'firstname',
'middle_name' => 'middlename',
'family_name' => 'lastname',
'email' => 'email',
'website' => 'url',
'nickname' => 'alternatename',
'picture' => 'picture',
'address' => 'address',
'phone' => 'phone1',
'locale' => 'lang'
];
foreach ($mapping as $external => $internal) {
$record = (object) [
'issuerid' => $issuer->get('id'),
'externalfield' => $external,
'internalfield' => $internal
];
$userfieldmapping = new user_field_mapping(0, $record);
$userfieldmapping->create();
}
return endpoint::count_records(['issuerid' => $issuer->get('id')]);
}
/**
* Take the data from the mform and update the issuer.
*
@@ -622,16 +522,7 @@ class api {
* @return \core\oauth2\issuer
*/
public static function update_issuer($data) {
require_capability('moodle/site:config', context_system::instance());
$issuer = new issuer(0, $data);
// Will throw exceptions on validation failures.
$issuer->update();
// Perform service discovery.
self::discover_endpoints($issuer);
self::guess_image($issuer);
return $issuer;
return self::create_or_update_issuer($data, false);
}
/**
@@ -641,18 +532,57 @@ class api {
* @return \core\oauth2\issuer
*/
public static function create_issuer($data) {
return self::create_or_update_issuer($data, true);
}
/**
* Take the data from the mform and create or update the issuer.
*
* @param stdClass $data Form data for them issuer to be created/updated.
* @param bool $create If true, the issuer will be created; otherwise, it will be updated.
* @return issuer The created/updated issuer.
*/
protected static function create_or_update_issuer($data, bool $create): issuer {
require_capability('moodle/site:config', context_system::instance());
$issuer = new issuer(0, $data);
// Will throw exceptions on validation failures.
$issuer->create();
if ($create) {
$issuer->create();
} else {
$issuer->update();
}
// Perform service discovery.
self::discover_endpoints($issuer);
$classname = self::get_service_classname($issuer->get('servicetype'));
$classname::discover_endpoints($issuer);
self::guess_image($issuer);
return $issuer;
}
/**
* Get the service classname for an issuer.
*
* @param string $type The OAuth issuer type (google, facebook...).
*
* @return string The classname for this issuer or "Custom" service class if the class for the defined type doesn't exist
* or null type is defined.
*/
protected static function get_service_classname(?string $type): string {
// Default custom service class.
$classname = 'core\\oauth2\\service\\custom';
if (!empty($type)) {
$typeclassname = 'core\\oauth2\\service\\' . $type;
if (class_exists($typeclassname)) {
$classname = $typeclassname;
}
}
return $classname;
}
/**
* Take the data from the mform and update the endpoint.
*
@@ -0,0 +1,164 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\oauth2\discovery;
use curl;
use stdClass;
use moodle_exception;
use core\oauth2\issuer;
use core\oauth2\endpoint;
/**
* Class for provider discovery definition, to allow services easily discover and process information.
* This abstract class is called from core\oauth2\api when discovery points need to be updated.
*
* @package core
* @since Moodle 3.11
* @copyright 2021 Sara Arjona ([email protected])
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base_definition {
/**
* Get the URL for the discovery manifest.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @return string The URL of the discovery file, containing the endpoints.
*/
public abstract static function get_discovery_endpoint_url(issuer $issuer): string;
/**
* Process the discovery information and create endpoints defined with the expected format.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @param stdClass $info The discovery information, with the endpoints to process and create.
* @return void
*/
protected abstract static function process_configuration_json(issuer $issuer, stdClass $info): void;
/**
* Process how to map user field information.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @return void
*/
protected abstract static function create_field_mappings(issuer $issuer): void;
/**
* Self-register the issuer if the 'registration' endpoint exists and client id and secret aren't defined.
*
* @param issuer $issuer The OAuth issuer to register.
* @return void
*/
protected abstract static function register(issuer $issuer): void;
/**
* Create endpoints for this issuer.
*
* @param issuer $issuer Issuer the endpoints should be created for.
* @return issuer
*/
public static function create_endpoints(issuer $issuer): issuer {
static::discover_endpoints($issuer);
return $issuer;
}
/**
* If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints.
*
* @param issuer $issuer
* @return int The number of discovered services.
*/
public static function discover_endpoints($issuer): int {
// Early return if baseurl is empty.
if (empty($issuer->get('baseurl'))) {
return 0;
}
// Get the discovery URL and check if it has changed.
$creatediscoveryendpoint = false;
$url = $issuer->get_endpoint_url('discovery');
$providerurl = static::get_discovery_endpoint_url($issuer);
if (!$url || $url != $providerurl) {
$url = $providerurl;
$creatediscoveryendpoint = true;
}
// Remove the existing endpoints before starting discovery.
foreach (endpoint::get_records(['issuerid' => $issuer->get('id')]) as $endpoint) {
// Discovery endpoint will be removed only if it will be created later, once we confirm it's working as expected.
if ($creatediscoveryendpoint || $endpoint->get('name') != 'discovery_endpoint') {
$endpoint->delete();
}
}
// Early return if discovery URL is empty.
if (empty($url)) {
return 0;
}
$curl = new curl();
if (!$json = $curl->get($url)) {
$msg = 'Could not discover end points for identity issuer: ' . $issuer->get('name') . " [URL: $url]";
throw new moodle_exception($msg);
}
if ($msg = $curl->error) {
throw new moodle_exception('Could not discover service endpoints: ' . $msg);
}
$info = json_decode($json);
if (empty($info)) {
$msg = 'Could not discover end points for identity issuer: ' . $issuer->get('name') . " [URL: $url]";
throw new moodle_exception($msg);
}
if ($creatediscoveryendpoint) {
// Create the discovery endpoint (because it didn't exist and the URL exists and is returning some valid JSON content).
static::create_discovery_endpoint($issuer, $url);
}
static::process_configuration_json($issuer, $info);
static::create_field_mappings($issuer);
static::register($issuer);
return endpoint::count_records(['issuerid' => $issuer->get('id')]);
}
/**
* Helper method to create discovery endpoint.
*
* @param issuer $issuer Issuer the endpoints should be created for.
* @param string $url Discovery endpoint URL.
* @return endpoint The endpoint created.
*
* @throws \core\invalid_persistent_exception
*/
protected static function create_discovery_endpoint(issuer $issuer, string $url): endpoint {
$record = (object) [
'issuerid' => $issuer->get('id'),
'name' => 'discovery_endpoint',
'url' => $url,
];
$endpoint = new endpoint(0, $record);
$endpoint->create();
return $endpoint;
}
}
@@ -0,0 +1,171 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\oauth2\discovery;
use curl;
use stdClass;
use moodle_exception;
use core\oauth2\issuer;
use core\oauth2\endpoint;
/**
* Class for IMS Open Badge Connect API (aka OBv2.1) discovery definition.
*
* @package core
* @since Moodle 3.11
* @copyright 2021 Sara Arjona ([email protected])
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class imsbadgeconnect extends base_definition {
/**
* Get the URL for the discovery manifest.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @return string The URL of the discovery file, containing the endpoints.
*/
public static function get_discovery_endpoint_url(issuer $issuer): string {
$url = $issuer->get('baseurl');
if (!empty($url)) {
// Add slash at the end of the base url.
$url .= (substr($url, -1) == '/' ? '' : '/');
// Append the well-known file for IMS OBv2.1.
$url .= '.well-known/badgeconnect.json';
}
return $url;
}
/**
* Process the discovery information and create endpoints defined with the expected format.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @param stdClass $info The discovery information, with the endpoints to process and create.
* @return void
*/
protected static function process_configuration_json(issuer $issuer, stdClass $info): void {
$info = array_pop($info->badgeConnectAPI);
foreach ($info as $key => $value) {
if (substr_compare($key, 'Url', - strlen('Url')) === 0 && !empty($value)) {
$record = new stdClass();
$record->issuerid = $issuer->get('id');
// Convert key names from xxxxUrl to xxxx_endpoint, in order to make it compliant with the Moodle oAuth API.
$record->name = strtolower(substr($key, 0, - strlen('Url'))) . '_endpoint';
$record->url = $value;
$endpoint = new endpoint(0, $record);
$endpoint->create();
} else if ($key == 'scopesOffered') {
// Get and update supported scopes.
$issuer->set('scopessupported', implode(' ', $value));
$issuer->update();
} else if ($key == 'image' && empty($issuer->get('image'))) {
// Update the image with the value in the manifest file if it's valid and empty in the issuer.
$url = filter_var($value, FILTER_SANITIZE_URL);
if (filter_var($url, FILTER_VALIDATE_URL) !== false) {
$issuer->set('image', $url);
$issuer->update();
}
}
}
}
/**
* Process how to map user field information.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @return void
*/
protected static function create_field_mappings(issuer $issuer): void {
// In that case, there are no user fields to map.
}
/**
* Self-register the issuer if the 'registration' endpoint exists and client id and secret aren't defined.
*
* @param issuer $issuer The OAuth issuer to register.
* @return void
*/
protected static function register(issuer $issuer): void {
global $CFG, $SITE;
$clientid = $issuer->get('clientid');
$clientsecret = $issuer->get('clientsecret');
// Registration request for getting client id and secret will be done only they are empty in the issuer.
// For now this can't be run from PHPUNIT (because IMS testing platform needs real URLs). In the future, this
// request can be moved to the moodle-exttests repository.
if (empty($clientid) && empty($clientsecret) && (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST)) {
$url = $issuer->get_endpoint_url('registration');
if ($url) {
$scopes = str_replace("\r", " ", $issuer->get('scopessupported'));
// Add slash at the end of the site URL.
$hosturl = $CFG->wwwroot;
$hosturl .= (substr($CFG->wwwroot, -1) == '/' ? '' : '/');
// Create the registration request following the format defined in the IMS OBv2.1 specification.
$request = [
'client_name' => $SITE->fullname,
'client_uri' => $hosturl,
'logo_uri' => $hosturl . 'pix/f/moodle-256.png',
'tos_uri' => $hosturl,
'policy_uri' => $hosturl,
'software_id' => 'moodle',
'software_version' => $CFG->version,
'redirect_uris' => [
$hosturl . 'admin/oauth2callback.php'
],
'token_endpoint_auth_method' => 'client_secret_basic',
'grant_types' => [
'authorization_code',
'refresh_token'
],
'response_types' => [
'code'
],
'scope' => $scopes
];
$jsonrequest = json_encode($request);
$curl = new curl();
$curl->setHeader(['Content-type: application/json']);
$curl->setHeader(['Accept: application/json']);
// Send the registration request.
if (!$jsonresponse = $curl->post($url, $jsonrequest)) {
$msg = 'Could not self-register identity issuer: ' . $issuer->get('name') .
". Wrong URL or JSON data [URL: $url]";
throw new moodle_exception($msg);
}
// Process the response and update client id and secret if they are valid.
$response = json_decode($jsonresponse);
if (property_exists($response, 'client_id')) {
$issuer->set('clientid', $response->client_id);
$issuer->set('clientsecret', $response->client_secret);
$issuer->update();
} else {
$msg = 'Could not self-register identity issuer: ' . $issuer->get('name') .
'. Invalid response ' . $jsonresponse;
throw new moodle_exception($msg);
}
}
}
}
}
@@ -0,0 +1,125 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\oauth2\discovery;
use stdClass;
use core\oauth2\issuer;
use core\oauth2\endpoint;
use core\oauth2\user_field_mapping;
/**
* Class for Open ID Connect discovery definition.
*
* @package core
* @since Moodle 3.11
* @copyright 2021 Sara Arjona ([email protected])
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class openidconnect extends base_definition {
/**
* Get the URL for the discovery manifest.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @return string The URL of the discovery file, containing the endpoints.
*/
public static function get_discovery_endpoint_url(issuer $issuer): string {
$url = $issuer->get('baseurl');
if (!empty($url)) {
// Add slash at the end of the base url.
$url .= (substr($url, -1) == '/' ? '' : '/');
// Append the well-known file for OIDC.
$url .= '.well-known/openid-configuration';
}
return $url;
}
/**
* Process the discovery information and create endpoints defined with the expected format.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @param stdClass $info The discovery information, with the endpoints to process and create.
* @return void
*/
protected static function process_configuration_json(issuer $issuer, stdClass $info): void {
foreach ($info as $key => $value) {
if (substr_compare($key, '_endpoint', - strlen('_endpoint')) === 0) {
$record = new stdClass();
$record->issuerid = $issuer->get('id');
$record->name = $key;
$record->url = $value;
$endpoint = new endpoint(0, $record);
$endpoint->create();
}
if ($key == 'scopes_supported') {
$issuer->set('scopessupported', implode(' ', $value));
$issuer->update();
}
}
}
/**
* Process how to map user field information.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @return void
*/
protected static function create_field_mappings(issuer $issuer): void {
// Remove existing user field mapping.
foreach (user_field_mapping::get_records(['issuerid' => $issuer->get('id')]) as $userfieldmapping) {
$userfieldmapping->delete();
}
// Create the default user field mapping list.
$mapping = [
'given_name' => 'firstname',
'middle_name' => 'middlename',
'family_name' => 'lastname',
'email' => 'email',
'website' => 'url',
'nickname' => 'alternatename',
'picture' => 'picture',
'address' => 'address',
'phone' => 'phone1',
'locale' => 'lang',
];
foreach ($mapping as $external => $internal) {
$record = (object) [
'issuerid' => $issuer->get('id'),
'externalfield' => $external,
'internalfield' => $internal
];
$userfieldmapping = new user_field_mapping(0, $record);
$userfieldmapping->create();
}
}
/**
* Self-register the issuer if the 'registration' endpoint exists and client id and secret aren't defined.
*
* @param issuer $issuer The OAuth issuer to register.
* @return void
*/
protected static function register(issuer $issuer): void {
// Registration not supported (at least for now).
}
}
+6 -1
View File
@@ -109,7 +109,12 @@ class issuer extends persistent {
'requireconfirmation' => array(
'type' => PARAM_BOOL,
'default' => true
)
),
'servicetype' => array(
'type' => PARAM_TEXT,
'null' => NULL_ALLOWED,
'default' => null,
),
);
}
+41
View File
@@ -0,0 +1,41 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\oauth2\service;
use core\oauth2\issuer;
use core\oauth2\discovery\openidconnect;
/**
* Class for Custom services, with the specific methods related to it.
*
* @package core
* @copyright 2021 Sara Arjona ([email protected])
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom extends openidconnect implements issuer_interface {
/**
* Build an OAuth2 issuer, with all the default values for this service.
*
* @return issuer|null The issuer initialised with proper default values.
*/
public static function init(): ?issuer {
// Custom service doesn't require any particular initialization.
return null;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\oauth2\service;
use core\oauth2\issuer;
use core\oauth2\discovery\openidconnect;
/**
* Class for Google oAuth service, with the specific methods related to it.
*
* @package core
* @copyright 2021 Sara Arjona ([email protected])
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class google extends openidconnect implements issuer_interface {
/**
* Build an OAuth2 issuer, with all the default values for this service.
*
* @return issuer|null The issuer initialised with proper default values.
*/
public static function init(): ?issuer {
$record = (object) [
'name' => 'Google',
'image' => 'https://accounts.google.com/favicon.ico',
'baseurl' => 'https://accounts.google.com/',
'loginparamsoffline' => 'access_type=offline&prompt=consent',
'showonloginpage' => true,
'servicetype' => 'google',
];
$issuer = new issuer(0, $record);
return $issuer;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\oauth2\service;
use core\oauth2\issuer;
use core\oauth2\discovery\imsbadgeconnect;
/**
* Class for IMS Open Badges v2.1 oAuth service, with the specific methods related to it.
*
* @package core
* @copyright 2021 Sara Arjona ([email protected])
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class imsobv2p1 extends imsbadgeconnect implements issuer_interface {
/**
* Build an OAuth2 issuer, with all the default values for this service.
*
* @return issuer|null The issuer initialised with proper default values.
*/
public static function init(): ?issuer {
$record = (object) [
'name' => 'IMS OBv2.1',
'image' => '',
'servicetype' => 'imsobv2p1',
];
$issuer = new issuer(0, $record);
return $issuer;
}
/**
* Process how to map user field information.
*
* @param issuer $issuer The OAuth issuer the endpoints should be discovered for.
* @return void
*/
public static function create_field_mappings(issuer $issuer): void {
// There are no specific field mappings for this service.
}
}
@@ -0,0 +1,53 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\oauth2\service;
use core\oauth2\issuer;
/**
* Interface for services, with the methods to be implemented by all the issuer implementing it.
*
* @package core
* @copyright 2021 Sara Arjona ([email protected])
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface issuer_interface {
/**
* Build an OAuth2 issuer, with all the default values for this service.
*
* @return issuer|null The issuer initialised with proper default values, or null if no issuer is initialised.
*/
public static function init(): ?issuer;
/**
* Create endpoints for this issuer.
*
* @param issuer $issuer Issuer the endpoints should be created for.
* @return issuer
*/
public static function create_endpoints(issuer $issuer): issuer;
/**
* If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints.
*
* @param issuer $issuer
* @return int The number of discovered services.
*/
public static function discover_endpoints($issuer): int;
}
+2 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20210127" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20210222" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@@ -3821,6 +3821,7 @@
<FIELD NAME="basicauth" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Use HTTP Basic authentication scheme when sending client ID and password"/>
<FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The defined sort order."/>
<FIELD NAME="requireconfirmation" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="servicetype" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Issuer service type, such as 'google' or 'facebook'."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+38
View File
@@ -2495,5 +2495,43 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2021030500.01);
}
if ($oldversion < 2021031200.01) {
// Define field type to be added to oauth2_issuer.
$table = new xmldb_table('oauth2_issuer');
$field = new xmldb_field('servicetype', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'requireconfirmation');
// Conditionally launch add field type.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Set existing values to the proper servicetype value.
// It's not critical if the servicetype column doesn't contain the proper value for Google, Microsoft, Facebook or
// Nextcloud services because, for now, this value is used for services using different discovery method.
// However, let's try to upgrade it using the default value for the baseurl or image. If any of these default values
// have been changed, the servicetype column will remain NULL.
$recordset = $DB->get_recordset('oauth2_issuer');
foreach ($recordset as $record) {
if ($record->baseurl == 'https://accounts.google.com/') {
$record->servicetype = 'google';
$DB->update_record('oauth2_issuer', $record);
} else if ($record->image == 'https://www.microsoft.com/favicon.ico') {
$record->servicetype = 'microsoft';
$DB->update_record('oauth2_issuer', $record);
} else if ($record->image == 'https://facebookbrand.com/wp-content/uploads/2016/05/flogo_rgb_hex-brc-site-250.png') {
$record->servicetype = 'facebook';
$DB->update_record('oauth2_issuer', $record);
} else if ($record->image == 'https://nextcloud.com/wp-content/themes/next/assets/img/common/favicon.png?x16328') {
$record->servicetype = 'nextcloud';
$DB->update_record('oauth2_issuer', $record);
}
}
$recordset->close();
// Main savepoint reached.
upgrade_main_savepoint(true, 2021031200.01);
}
return true;
}
+114
View File
@@ -252,4 +252,118 @@ class core_oauth2_testcase extends advanced_testcase {
$this->assertTrue($issuer->is_valid_login_domain('[email protected]'));
}
/**
* Test endpoints creation for issuers.
* @dataProvider create_endpoints_for_standard_issuer_provider
*
* @covers ::create_endpoints_for_standard_issuer
*
* @param string $type Issuer type to create.
* @param string|null $discoveryurl Expected discovery URL or null if this endpoint doesn't exist.
* @param bool $hasmappingfields True if it's expected the issuer to create has mapping fields.
* @param string|null $baseurl The service URL (mandatory parameter for some issuers, such as NextCloud or IMS OBv2.1).
* @param string|null $expectedexception Name of the expected expection or null if no exception will be thrown.
*/
public function test_create_endpoints_for_standard_issuer(string $type, ?string $discoveryurl = null,
bool $hasmappingfields = true, ?string $baseurl = null, ?string $expectedexception = null): void {
$this->resetAfterTest();
// Mark test as long because it connects with external services.
if (!PHPUNIT_LONGTEST) {
$this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
}
$this->setAdminUser();
// Method create_endpoints_for_standard_issuer is called internally from create_standard_issuer.
if ($expectedexception) {
$this->expectException($expectedexception);
}
$issuer = \core\oauth2\api::create_standard_issuer($type, $baseurl);
// Check endpoints have been created.
$endpoints = \core\oauth2\api::get_endpoints($issuer);
$this->assertNotEmpty($endpoints);
$this->assertNotEmpty($issuer->get('image'));
// Check discovery URL.
if ($discoveryurl) {
$this->assertStringContainsString($discoveryurl, $issuer->get_endpoint_url('discovery'));
} else {
$this->assertFalse($issuer->get_endpoint_url('discovery'));
}
// Check userfield mappings.
$userfieldmappings = core\oauth2\api::get_user_field_mappings($issuer);
if ($hasmappingfields) {
$this->assertNotEmpty($userfieldmappings);
} else {
$this->assertEmpty($userfieldmappings);
}
}
/**
* Data provider for test_create_endpoints_for_standard_issuer.
*
* @return array
*/
public function create_endpoints_for_standard_issuer_provider(): array {
return [
'Google' => [
'type' => 'google',
'discoveryurl' => '.well-known/openid-configuration',
],
'Google will work too with a valid baseurl parameter' => [
'type' => 'google',
'discoveryurl' => '.well-known/openid-configuration',
'hasmappingfields' => true,
'baseurl' => 'https://accounts.google.com/',
],
'IMS OBv2.1' => [
'type' => 'imsobv2p1',
'discoveryurl' => '.well-known/badgeconnect.json',
'hasmappingfields' => false,
'baseurl' => 'https://dc.imsglobal.org/',
],
'IMS OBv2.1 without slash in baseurl should work too' => [
'type' => 'imsobv2p1',
'discoveryurl' => '.well-known/badgeconnect.json',
'hasmappingfields' => false,
'baseurl' => 'https://dc.imsglobal.org',
],
'IMS OBv2.1 with empty baseurl should return an exception' => [
'type' => 'imsobv2p1',
'discoveryurl' => null,
'hasmappingfields' => false,
'baseurl' => null,
'expectedexception' => \moodle_exception::class,
],
'Microsoft' => [
'type' => 'microsoft',
],
'Facebook' => [
'type' => 'facebook',
],
'NextCloud' => [
'type' => 'nextcloud',
'discoveryurl' => null,
'hasmappingfields' => true,
'baseurl' => 'https://dummy.local/nextcloud/',
],
'NextCloud with empty baseurl should return an exception' => [
'type' => 'nextcloud',
'discoveryurl' => null,
'hasmappingfields' => true,
'baseurl' => null,
'expectedexception' => \moodle_exception::class,
],
'Invalid type should return an exception' => [
'type' => 'fictitious',
'discoveryurl' => null,
'hasmappingfields' => true,
'baseurl' => null,
'expectedexception' => \moodle_exception::class,
],
];
}
}