diff --git a/src/main/java/org/olat/ims/lti/LTIManager.java b/src/main/java/org/olat/ims/lti/LTIManager.java index aa28560ab8eae91e2a256760095b56c93e66bd4d..7842eb43a97aadef892c6ea3e39567a8d7fbbb50 100644 --- a/src/main/java/org/olat/ims/lti/LTIManager.java +++ b/src/main/java/org/olat/ims/lti/LTIManager.java @@ -56,5 +56,12 @@ public interface LTIManager { */ public void deleteOutcomes(OLATResource resource); + /** + * Make a LTI request with a HTTP Post request. + * @param signedProps the signed LTI properties + * @param url the url to send the request + * @return the http response content as string or null if the request was not successful + */ + public String post(Map<String,String> signedProps, String url); } diff --git a/src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java b/src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java index c3e2368237d5d5a45cf3fba22cbf91f59eb8a507..174e1aee4ff07450386783e8669f8586b9c453cc 100644 --- a/src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java +++ b/src/main/java/org/olat/ims/lti/manager/LTIManagerImpl.java @@ -24,9 +24,19 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.stream.Collectors; import javax.persistence.TypedQuery; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; import org.imsglobal.basiclti.BasicLTIUtil; import org.olat.basesecurity.Authentication; import org.olat.basesecurity.BaseSecurityManager; @@ -35,6 +45,8 @@ import org.olat.core.helpers.Settings; import org.olat.core.id.Identity; import org.olat.core.id.User; import org.olat.core.id.UserConstants; +import org.olat.core.logging.OLog; +import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.WebappHelper; import org.olat.ims.lti.LTIContext; @@ -55,6 +67,8 @@ import org.springframework.stereotype.Service; */ @Service public class LTIManagerImpl implements LTIManager { + + private static final OLog log = Tracing.createLoggerFor(LTIManagerImpl.class); @Autowired private DB dbInstance; @@ -129,8 +143,12 @@ public class LTIManagerImpl implements LTIManager { String tool_consumer_instance_url = null; String tool_consumer_instance_name = WebappHelper.getInstanceId(); String tool_consumer_instance_contact_email = WebappHelper.getMailConfig("mailSupport"); + + if (props == null) { + props = new HashMap<>(); + } - Map<String,String> signedProps = BasicLTIUtil.signProperties(props, url, "POST", + return BasicLTIUtil.signProperties(props, url, "POST", oauth_consumer_key, oauth_consumer_secret, tool_consumer_instance_guid, @@ -138,15 +156,13 @@ public class LTIManagerImpl implements LTIManager { tool_consumer_instance_url, tool_consumer_instance_name, tool_consumer_instance_contact_email); - - return signedProps; } @Override public Map<String,String> forgeLTIProperties(Identity identity, Locale locale, LTIContext context, boolean sendName, boolean sendEmail) { - final Identity ident = identity; final Locale loc = locale; + final Identity ident = identity; final User u = ident.getUser(); final String lastName = u.getProperty(UserConstants.LASTNAME, loc); final String firstName = u.getProperty(UserConstants.FIRSTNAME, loc); @@ -208,7 +224,7 @@ public class LTIManagerImpl implements LTIManager { if (!StringHelper.containsNonWhitespace(param)) { continue; } - int pos = param.indexOf("="); + int pos = param.indexOf('='); if (pos < 1 || pos + 1 > param.length()) { continue; } @@ -291,5 +307,28 @@ public class LTIManagerImpl implements LTIManager { } return personSourceId; } + + @Override + public String post(Map<String,String> signedProps, String url) { + String content = null; + + // Map the LTI properties to HttpClient parameters + List<NameValuePair> urlParameters = signedProps.keySet().stream() + .map(k -> new BasicNameValuePair(k, signedProps.get(k))) + .collect(Collectors.toList()); + + // make the http request and evaluate the result + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpPost request = new HttpPost(url); + HttpEntity postParams = new UrlEncodedFormEntity(urlParameters); + request.setEntity(postParams); + HttpResponse httpResponse = httpclient.execute(request); + content = IOUtils.toString(httpResponse.getEntity().getContent()); + } catch (Exception e) { + log.error("", e); + } + + return content; + } } diff --git a/src/main/java/org/olat/modules/card2brain/Card2BrainManager.java b/src/main/java/org/olat/modules/card2brain/Card2BrainManager.java index 3279207dba781832093924ac1a265fc3ca8fc684..246cba6173d5bfa15f08aad17109f997f9961940 100644 --- a/src/main/java/org/olat/modules/card2brain/Card2BrainManager.java +++ b/src/main/java/org/olat/modules/card2brain/Card2BrainManager.java @@ -19,6 +19,8 @@ */ package org.olat.modules.card2brain; +import org.olat.modules.card2brain.manager.Card2BrainVerificationResult; + /** * * Initial date: 20.04.2017<br> @@ -33,5 +35,15 @@ public interface Card2BrainManager { * @return true if the set of flashcards exists. */ public boolean checkSetOfFlashcards(String alias); + + /** + * Verify if the key and the secret of the enterprise login are valid. + * + * @param url the url of the verification service + * @param key the key + * @param secret the secret + * @return Card2BrainVerificationResult the result of the verification + */ + public Card2BrainVerificationResult checkEnterpriseLogin(String url, String key, String secret); } diff --git a/src/main/java/org/olat/modules/card2brain/Card2BrainModule.java b/src/main/java/org/olat/modules/card2brain/Card2BrainModule.java index edfdada5b479c279bcf417ed7d1acd3c1ec8b710..cebb319948118e004ae88f4855d022f3957273a9 100644 --- a/src/main/java/org/olat/modules/card2brain/Card2BrainModule.java +++ b/src/main/java/org/olat/modules/card2brain/Card2BrainModule.java @@ -42,6 +42,7 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf public static final String CARD2BRAIN_ENTERPRISE_SECRET= "card2brain.enterpriseSecret"; public static final String CARD2BRAIN_BASE_URL = "card2brain.baseUrl"; public static final String CARD2BRAIN_PEEK_VIEW_URL = "card2brain.peekViewUrl"; + public static final String CARD2BRAIN_VERIFY_LTI_URL = "card2brain.verifyLtiUrl"; @Value("${card2brain.enabled:false}") private boolean enabled; @@ -53,6 +54,8 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf private String baseUrl; @Value("${card2brain.peekViewUrl:null}") private String peekViewUrl; + @Value("${card2brain.verifyLtiUrl:null}") + private String verifyLtiUrl; @Autowired public Card2BrainModule(CoordinatorManager coordinatorManager) { @@ -90,6 +93,11 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf if(StringHelper.containsNonWhitespace(peekViewUrlObj)) { peekViewUrl = peekViewUrlObj; } + + String verifyLtiUrlObj = getStringPropertyValue(CARD2BRAIN_VERIFY_LTI_URL, true); + if(StringHelper.containsNonWhitespace(verifyLtiUrlObj)) { + verifyLtiUrl = verifyLtiUrlObj; + } } @Override @@ -152,6 +160,15 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf setStringProperty(CARD2BRAIN_PEEK_VIEW_URL, peekViewUrl, true); } + public String getVerifyLtiUrl() { + return verifyLtiUrl; + } + + public void setVerifyLtiUrl(String verifyLtiUrl) { + this.verifyLtiUrl = verifyLtiUrl; + setStringProperty(CARD2BRAIN_VERIFY_LTI_URL, verifyLtiUrl, true); + } + /** * Check if the use of a certain login is safe.<br> * - If the enterprise login is enable, it is safe to use a enterprise login.<br> diff --git a/src/main/java/org/olat/modules/card2brain/manager/Card2BrainManagerImpl.java b/src/main/java/org/olat/modules/card2brain/manager/Card2BrainManagerImpl.java index 2f487ea057dea46bf803f8304589a0849734944c..41dcc2b31c0100de3aaacf516009d0ae2c6a9f30 100644 --- a/src/main/java/org/olat/modules/card2brain/manager/Card2BrainManagerImpl.java +++ b/src/main/java/org/olat/modules/card2brain/manager/Card2BrainManagerImpl.java @@ -19,14 +19,19 @@ */ package org.olat.modules.card2brain.manager; +import java.util.Map; + import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; +import org.codehaus.jackson.JsonParseException; +import org.codehaus.jackson.map.ObjectMapper; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; +import org.olat.ims.lti.LTIManager; import org.olat.modules.card2brain.Card2BrainManager; import org.olat.modules.card2brain.Card2BrainModule; import org.springframework.beans.factory.annotation.Autowired; @@ -45,6 +50,9 @@ public class Card2BrainManagerImpl implements Card2BrainManager { @Autowired private Card2BrainModule card2brainModule; + + @Autowired + private LTIManager ltiManager; @Override public boolean checkSetOfFlashcards(String alias) { @@ -67,4 +75,22 @@ public class Card2BrainManagerImpl implements Card2BrainManager { return setOfFlashcardExists; } + @Override + public Card2BrainVerificationResult checkEnterpriseLogin(String url, String key, String secret) { + Card2BrainVerificationResult card2BrainValidationResult = null; + + try { + Map<String,String> signedPros = ltiManager.sign(null, url, key, secret); + String content = ltiManager.post(signedPros, url); + ObjectMapper mapper = new ObjectMapper(); + card2BrainValidationResult = mapper.readValue(content, Card2BrainVerificationResult.class); + } catch (JsonParseException jsonParseException) { + // ignore and return null + } catch (Exception e) { + log.error("", e); + } + + return card2BrainValidationResult; + } + } diff --git a/src/main/java/org/olat/modules/card2brain/manager/Card2BrainVerificationResult.java b/src/main/java/org/olat/modules/card2brain/manager/Card2BrainVerificationResult.java new file mode 100644 index 0000000000000000000000000000000000000000..b84791668fa72f91f2b83b433e20418071ffd89d --- /dev/null +++ b/src/main/java/org/olat/modules/card2brain/manager/Card2BrainVerificationResult.java @@ -0,0 +1,59 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.card2brain.manager; + +/** + * Data object to encapsulate a result of a verification. + * + * Initial date: 09.05.2017<br> + * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com + * + */ +public class Card2BrainVerificationResult { + + private boolean success; + private String message; + + public Card2BrainVerificationResult(boolean valid, String message) { + this.success = valid; + this.message = message; + } + + public Card2BrainVerificationResult() { + // nothing to do + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean valid) { + this.success = valid; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + +} diff --git a/src/main/java/org/olat/modules/card2brain/ui/Card2BrainAdminController.java b/src/main/java/org/olat/modules/card2brain/ui/Card2BrainAdminController.java index 71e8aa2e293691cafb70b82287f865e91303a286..24cd211e5d74d761b9e588d49fdc8fb45e73953f 100644 --- a/src/main/java/org/olat/modules/card2brain/ui/Card2BrainAdminController.java +++ b/src/main/java/org/olat/modules/card2brain/ui/Card2BrainAdminController.java @@ -22,6 +22,7 @@ package org.olat.modules.card2brain.ui; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement; import org.olat.core.gui.components.form.flexible.elements.TextElement; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; @@ -30,7 +31,9 @@ import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.WindowControl; import org.olat.core.util.StringHelper; +import org.olat.modules.card2brain.Card2BrainManager; import org.olat.modules.card2brain.Card2BrainModule; +import org.olat.modules.card2brain.manager.Card2BrainVerificationResult; import org.springframework.beans.factory.annotation.Autowired; /** @@ -48,11 +51,15 @@ public class Card2BrainAdminController extends FormBasicController { private MultipleSelectionElement enterpriseLoginEnabledEl; private TextElement enterpriseKeyEl; private TextElement enterpriseSecretEl; + private FormLink checkLoginButton; private TextElement baseUrlEl; private TextElement peekViewUrlEl; + private TextElement verifyLtiUrlEl; @Autowired private Card2BrainModule card2BrainModule; + @Autowired + private Card2BrainManager card2BrainManager; public Card2BrainAdminController(UserRequest ureq, WindowControl wControl) { super(ureq, wControl); @@ -90,6 +97,9 @@ public class Card2BrainAdminController extends FormBasicController { enterpriseSecretEl = uifactory.addPasswordElement("admin.enterpriseSecret", "admin.enterpriseSecret", 128, enterpriseSecret, formLayout); enterpriseSecretEl.setMandatory(true); + checkLoginButton = uifactory.addFormLink("admin.verifyKeySecret.button", formLayout, "btn btn-default"); + checkLoginButton.getComponent().setSuppressDirtyFormWarning(true); // doesn't work + uifactory.addSpacerElement("Spacer", formLayout, false); uifactory.addStaticTextElement("admin.expertSettings", null, formLayout); @@ -103,6 +113,10 @@ public class Card2BrainAdminController extends FormBasicController { peekViewUrlEl.setMandatory(true); peekViewUrlEl.setHelpTextKey("admin.peekViewUrlHelpText", null); + String verifyLtiUrl = card2BrainModule.getVerifyLtiUrl(); + verifyLtiUrlEl = uifactory.addTextElement("admin.verifyKeySecret.url", "admin.verifyKeySecret.url", 128, verifyLtiUrl, formLayout); + verifyLtiUrlEl.setMandatory(true); + FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); formLayout.add("buttons", buttonLayout); uifactory.addFormSubmitButton("save", buttonLayout); @@ -112,8 +126,10 @@ public class Card2BrainAdminController extends FormBasicController { @Override protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) { - if(enterpriseLoginEnabledEl == source) { + if (enterpriseLoginEnabledEl == source) { showHideEnterpriseLoginFields(); + } else if (checkLoginButton == source) { + checkKeySecret(); } super.formInnerEvent(ureq, source, event); } @@ -132,11 +148,14 @@ public class Card2BrainAdminController extends FormBasicController { String enterpriseSecret = enterpriseSecretEl.getValue(); card2BrainModule.setEnterpriseSecret(enterpriseSecret); - String baseURL = baseUrlEl.getValue(); - card2BrainModule.setBaseUrl(baseURL); + String baseUrl = baseUrlEl.getValue(); + card2BrainModule.setBaseUrl(baseUrl); + + String peekViewUrl = peekViewUrlEl.getValue(); + card2BrainModule.setPeekViewUrl(peekViewUrl); - String peekViewURL = peekViewUrlEl.getValue(); - card2BrainModule.setPeekViewUrl(peekViewURL); + String verifyLtiUrl = verifyLtiUrlEl.getValue(); + card2BrainModule.setVerifyLtiUrl(verifyLtiUrl); } @Override @@ -145,47 +164,23 @@ public class Card2BrainAdminController extends FormBasicController { //validate only if the module is enabled if(card2BrainModule.isEnabled()) { - allOk &= validateEnterpriseLogin(); - allOk &= validateBaseUrl(); - allOk &= validatePeekViewUrl(); - } - - return allOk & super.validateFormLogic(ureq); - } - - private boolean validateEnterpriseLogin() { - boolean allOk = true; - - if (isEnterpriseLoginEnabled()) { - if (!StringHelper.containsNonWhitespace(enterpriseKeyEl.getValue())) { - enterpriseKeyEl.setErrorKey(FORM_MISSING_MANDATORY, null); - allOk &= false; - } - if (!StringHelper.containsNonWhitespace(enterpriseSecretEl.getValue())) { - enterpriseSecretEl.setErrorKey(FORM_MISSING_MANDATORY, null); - allOk &= false; + if (isEnterpriseLoginEnabled()) { + allOk &= validateIsMandatory(enterpriseKeyEl); + allOk &= validateIsMandatory(enterpriseSecretEl); } + allOk &= validateIsMandatory(baseUrlEl); + allOk &= validateIsMandatory(peekViewUrlEl); + allOk &= validateIsMandatory(verifyLtiUrlEl); } - return allOk; + return allOk & super.validateFormLogic(ureq); } - private boolean validateBaseUrl() { + private boolean validateIsMandatory(TextElement textElement) { boolean allOk = true; - if (!StringHelper.containsNonWhitespace(baseUrlEl.getValue())) { - baseUrlEl.setErrorKey(FORM_MISSING_MANDATORY, null); - allOk &= false; - } - - return allOk; - } - - private boolean validatePeekViewUrl() { - boolean allOk = true; - - if (!StringHelper.containsNonWhitespace(peekViewUrlEl.getValue())) { - peekViewUrlEl.setErrorKey(FORM_MISSING_MANDATORY, null); + if (!StringHelper.containsNonWhitespace(textElement.getValue())) { + textElement.setErrorKey(FORM_MISSING_MANDATORY, null); allOk &= false; } @@ -201,6 +196,22 @@ public class Card2BrainAdminController extends FormBasicController { return enterpriseLoginEnabledEl.isAtLeastSelected(1); } + private void checkKeySecret() { + String verifyLtiUrl = verifyLtiUrlEl.getValue(); + String key = enterpriseKeyEl.getValue(); + String secret = enterpriseSecretEl.getValue(); + + Card2BrainVerificationResult verification = + card2BrainManager.checkEnterpriseLogin(verifyLtiUrl, key, secret); + if(verification == null) { + showError("admin.verifyKeySecret.unavaible"); + } else if (verification.isSuccess()) { + showInfo("admin.verifyKeySecret.valid"); + } else { + showError("admin.verifyKeySecret.invalid", verification.getMessage()); + } + } + @Override protected void doDispose() { // diff --git a/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_de.properties index b31f7689ee0501b4986af974c6f959dd7446b71a..8295fb3f4bf6c5865f8768b836eeed5267a6321b 100644 --- a/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_de.properties @@ -14,3 +14,9 @@ admin.menu.title.alt=card2brain admin.peekViewUrl=URL der Vorschau admin.peekViewUrlHelpText=Als Platzhalter f\u00FCr das Alias der Lernkartei ist '%s' zu verwenden. admin.title=Konfiguration +admin.verifyKeySecret.button=Key/Secret \u00FCberpr\u00FCfen +admin.verifyKeySecret.invalid=Der Key und das Secret sind nicht g\u00FCltig. Anwort vom card2brain-Server: {0} +admin.verifyKeySecret.unavaible=Die Pr\u00FCfung konnte nicht durchgef\u00FChrt werden. +admin.verifyKeySecret.url=URL LTI Key/Secret Verifizierung +admin.verifyKeySecret.valid=Der Key und das Secret sind g\u00FCltig. + diff --git a/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_en.properties index b55a2f65864c201e435c2dabe7abbf2035d2bced..9cf8a79acdbe15a45d5cc2f64b6c47d89e15c4a5 100644 --- a/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/modules/card2brain/ui/_i18n/LocalStrings_en.properties @@ -14,3 +14,8 @@ admin.menu.title.alt=card2brain admin.peekViewUrl=URL peek view admin.peekViewUrlHelpText=Use '%s' as a placeholder for the alias of the flashcards. admin.title=Configuration +admin.verifyKeySecret.button=Verify Key/Secret +admin.verifyKeySecret.invalid=Key and Secret are invalid. Response from card2brain server: {0} +admin.verifyKeySecret.unavaible=The verification was not executed correctly. +admin.verifyKeySecret.url=URL LTI Key/Secret verification +admin.verifyKeySecret.valid=Key and Secret are valid. diff --git a/src/main/resources/serviceconfig/olat.properties b/src/main/resources/serviceconfig/olat.properties index 805fc1a5d16aaabf735e6a929350c72567a1b626..3356595d9f7890f4c98227fc0e9c6d66fd3440dd 100644 --- a/src/main/resources/serviceconfig/olat.properties +++ b/src/main/resources/serviceconfig/olat.properties @@ -1218,6 +1218,7 @@ card2brain.enabled=false card2brain.enterpriseLoginEnabled=false card2brain.baseUrl=https://card2brain.ch/grails/SSO/lti.dispatch?alias=%s card2brain.peekViewUrl=https://card2brain.ch/box/%s/embed +card2brain.verifyLtiUrl=https://card2brain.ch/grails/SSO/verifyLti.dispatch ######################################## # Options for monitoring