From 25dab7336707926c685207e20af02b343b1b0ba7 Mon Sep 17 00:00:00 2001
From: Charles Severance
Date: Tue, 11 Oct 2022 11:22:05 -0400
Subject: [PATCH] SAK-47852 Plus (LTI Advantage Provider) initial addition to
Sakai (#10877)
https://sakaiproject.atlassian.net/browse/SAK-47852
---
assignment/impl/pom.xml | 1 -
assignment/tool/pom.xml | 2 -
basiclti/basiclti-blis/pom.xml | 1 -
.../org/sakaiproject/lti13/LTI13Servlet.java | 65 +-
basiclti/basiclti-common/pom.xml | 9 +-
.../basiclti/util/SakaiBLTIUtil.java | 28 +-
.../basiclti/util/SakaiContentItemUtil.java | 24 +-
.../org/sakaiproject/lti13/LineItemUtil.java | 6 +-
.../lti13/util/SakaiExtension.java | 9 +-
.../resources/docs/LTI_CertPlan.xls | Bin 33792 -> 0 bytes
basiclti/basiclti-impl/pom.xml | 1 -
.../src/bundle/ltiservice.properties | 4 +-
.../impl/BasicLTISecurityServiceImpl.java | 35 +-
.../lti/impl/LTIRoleMapperImpl.java | 213 +--
.../lti/impl/UserFinderOrCreatorImpl.java | 219 ++-
.../src/bundle/basiclti.properties | 1 +
basiclti/basiclti-tool/pom.xml | 5 +-
.../src/webapp/vm/lti_tool_insert.vm | 1 +
basiclti/docs/CERTIFICATION.md | 32 +
basiclti/docs/CERTIFICATION_22.pdf | Bin 0 -> 756995 bytes
basiclti/docs/CUSTOM.md | 41 +
basiclti/docs/README.md | 1 +
basiclti/pom.xml | 8 -
basiclti/tsugi-util/pom.xml | 26 +-
.../src/java/org/tsugi/HACK/HackMoodle.java | 72 +
.../src/java/org/tsugi/HACK/README.md | 10 +
.../java/org/tsugi/ags2/objects/LineItem.java | 17 +-
.../java/org/tsugi/ags2/objects/Result.java | 3 -
.../java/org/tsugi/ags2/objects/Score.java | 7 +-
.../tsugi/ags2/objects/SubmissionReview.java | 3 -
.../java/org/tsugi/basiclti/BasicLTIUtil.java | 102 +-
.../objects/ContentItemResponse.java | 3 -
.../org/tsugi/contentitem/objects/Icon.java | 3 -
.../contentitem/objects/LtiLinkItem.java | 3 -
.../contentitem/objects/PlacementAdvice.java | 3 -
.../tsugi/deeplink/objects/ContentItem.java | 130 ++
.../deeplink/objects/DeepLinkResponse.java | 157 ++
.../deeplink/objects/LtiResourceLink.java | 55 +
.../tsugi/deeplink/objects/MiniLineItem.java | 32 +
.../java/org/tsugi/http/HttpClientUtil.java | 215 +++
.../src/java/org/tsugi/http/HttpUtil.java | 40 +-
.../java/org/tsugi/jackson/JacksonUtil.java | 11 +
.../org/tsugi/lti13/DeepLinkResponse.java | 1 -
.../org/tsugi/lti13/LTI13AccessTokenUtil.java | 222 +++
.../org/tsugi/lti13/LTI13ConstantsUtil.java | 18 +
.../java/org/tsugi/lti13/LTI13KeySetUtil.java | 18 +-
.../src/java/org/tsugi/lti13/LTI13Util.java | 13 +
.../java/org/tsugi/lti13/objects/BaseJWT.java | 33 +-
.../org/tsugi/lti13/objects/BasicOutcome.java | 6 +-
.../java/org/tsugi/lti13/objects/Context.java | 5 +-
.../org/tsugi/lti13/objects/DeepLink.java | 5 +-
.../org/tsugi/lti13/objects/Endpoint.java | 22 +-
.../java/org/tsugi/lti13/objects/ForUser.java | 5 +-
.../tsugi/lti13/objects/LTI11Transition.java | 5 +-
...formMessage.java => LTILaunchMessage.java} | 13 +-
.../objects/LTIPlatformConfiguration.java | 9 +-
.../lti13/objects/LTIToolConfiguration.java | 81 +-
.../org/tsugi/lti13/objects/LaunchJWT.java | 53 +-
.../org/tsugi/lti13/objects/LaunchLIS.java | 7 +-
.../lti13/objects/LaunchPresentation.java | 5 +-
.../tsugi/lti13/objects/NamesAndRoles.java | 5 +-
...ion.java => OpenIDClientRegistration.java} | 56 +-
....java => OpenIDProviderConfiguration.java} | 16 +-
.../org/tsugi/lti13/objects/ResourceLink.java | 5 +-
.../org/tsugi/lti13/objects/ToolPlatform.java | 5 +-
.../org/tsugi/nrps/objects/Container.java | 73 +
.../java/org/tsugi/nrps/objects/Member.java | 87 +
.../org/tsugi/nrps/objects/MemberMessage.java | 44 +
.../org/tsugi/oauth2/objects/AccessToken.java | 24 +-
.../tsugi/oauth2/objects/ClientAssertion.java | 48 +
.../objects/RegistrationRequest.java | 83 +
.../objects/RegistrationResponse.java | 72 +
.../org/tsugi/shared/objects/Contact.java | 3 -
.../org/tsugi/shared/objects/DateRange.java | 27 +
.../org/tsugi/shared/objects/SizedUrl.java | 31 +
.../org/tsugi/shared/objects/TsugiBase.java | 4 -
.../src/java/org/tsugi/time/InstantUtil.java | 67 +
.../test/org/tsugi/HACK/MoodleHackTest.java | 29 +
.../deeplink/DeepLinkResponseObjectTest.java | 96 +
.../src/test/org/tsugi/http/HttpUtilTest.java | 23 +
.../tsugi/lti13/LTI13AccessTokenUtilTest.java | 114 ++
.../test/org/tsugi/lti13/LTI13ObjectTest.java | 57 +-
.../test/org/tsugi/lti13/LTI13UtilTest.java | 12 +
.../src/test/org/tsugi/nrps/NRPSTest.java | 78 +
.../org/tsugi/oauth2/OAUTH2ObjectTest.java | 48 +-
.../test/org/tsugi/time/InstantUtilTest.java | 65 +
.../deeplink/deep_link_settings.json | 12 +
.../deeplink/sample_ltiresourcelink.json | 39 +
.../resources/deeplink/sample_response.json | 106 ++
.../test/resources/nrps/sample_container.json | 39 +
.../test/resources/nrps/sample_member.json | 29 +
.../resources/nrps/sample_member_message.json | 11 +
.../resources/oauth2/sample_access_token.json | 6 +
.../config/bundle/default.sakai.properties | 123 ++
.../webapp/WEB-INF/templates/bootstrap.html | 5 +-
deploy/pom.xml | 5 +
.../sakaiproject/grading/api/Assignment.java | 1 +
.../api/model/GradebookAssignment.java | 3 +
gradebookng/impl/pom.xml | 16 +
.../grading/impl/GradingServiceImpl.java | 217 ++-
.../src/main/webapp/WEB-INF/components.xml | 3 +-
.../impl/test/GradingServiceTests.java | 2 +
.../impl/test/GradingTestConfiguration.java | 6 +
.../src/main/sql/hsqldb/sakai_site.sql | 9 +
.../src/main/sql/mysql/sakai_site.sql | 6 +
.../src/main/sql/oracle/sakai_site.sql | 9 +
.../images/logo-jewel-square.png | Bin 0 -> 29997 bytes
.../src/morpheus-master/sass/base/_icons.scss | 1 +
library/src/webapp/js/headscripts.js | 23 +-
library/src/webapp/js/waterfall-light.js | 207 +++
master/pom.xml | 60 +-
plus/README.md | 85 +
plus/SQL.md | 105 ++
plus/TODO.md | 40 +
plus/api/pom.xml | 54 +
.../org/sakaiproject/plus/api/Launch.java | 38 +
.../sakaiproject/plus/api/PlusService.java | 177 ++
.../sakaiproject/plus/api/model/BaseLTI.java | 105 ++
.../sakaiproject/plus/api/model/Context.java | 108 ++
.../plus/api/model/ContextLog.java | 105 ++
.../sakaiproject/plus/api/model/LineItem.java | 98 +
.../org/sakaiproject/plus/api/model/Link.java | 76 +
.../plus/api/model/Membership.java | 68 +
.../sakaiproject/plus/api/model/Score.java | 128 ++
.../sakaiproject/plus/api/model/Subject.java | 77 +
.../sakaiproject/plus/api/model/Tenant.java | 166 ++
.../api/repository/ContextLogRepository.java | 31 +
.../api/repository/ContextRepository.java | 33 +
.../api/repository/LineItemRepository.java | 24 +
.../plus/api/repository/LinkRepository.java | 27 +
.../api/repository/MembershipRepository.java | 30 +
.../plus/api/repository/ScoreRepository.java | 29 +
.../api/repository/SubjectRepository.java | 31 +
.../plus/api/repository/TenantRepository.java | 27 +
plus/docs/COMPLETED.md | 52 +
plus/docs/INSTALL-BLACKBOARD.md | 159 ++
plus/docs/INSTALL-BRIGHTSPACE.md | 50 +
plus/docs/INSTALL-CANVAS.md | 50 +
plus/docs/INSTALL-MOODLE.md | 21 +
plus/docs/INSTALL-SAKAI.md | 62 +
plus/docs/NOTES.md | 16 +
plus/docs/TESTING.md | 136 ++
plus/impl/pom.xml | 155 ++
.../plus/impl/PlusEventObserver.java | 69 +
.../plus/impl/PlusServiceImpl.java | 1426 ++++++++++++++
.../impl/jobs/SiteMembershipsSyncJob.java | 39 +
.../repository/ContextLogRepositoryImpl.java | 90 +
.../repository/ContextRepositoryImpl.java | 89 +
.../repository/LineItemRepositoryImpl.java | 25 +
.../impl/repository/LinkRepositoryImpl.java | 52 +
.../repository/MembershipRepositoryImpl.java | 81 +
.../impl/repository/ScoreRepositoryImpl.java | 71 +
.../repository/SubjectRepositoryImpl.java | 132 ++
.../impl/repository/TenantRepositoryImpl.java | 63 +
.../src/main/webapp/WEB-INF/components.xml | 86 +
.../plus/impl/PlusModelTests.java | 521 ++++++
.../plus/impl/PlusServiceImplTests.java | 55 +
.../plus/impl/PlusTestConfiguration.java | 205 ++
.../src/test/resources/hibernate.properties | 19 +
plus/pom.xml | 63 +
plus/provider/maven.xml | 13 +
plus/provider/pom.xml | 146 ++
plus/provider/src/bundle/plus.properties | 176 ++
.../sakaiproject/plus/ProviderServlet.java | 1653 +++++++++++++++++
.../webapp/WEB-INF/applicationContext.xml | 12 +
.../src/main/webapp/WEB-INF/descriptor.json | 103 +
plus/provider/src/main/webapp/WEB-INF/web.xml | 35 +
plus/provider/src/main/webapp/deeplink.jsp | 89 +
plus/provider/src/main/webapp/descriptor.txt | 21 +
plus/provider/src/main/webapp/whatisthis.htm | 53 +
.../plus/provider/ProviderTests.java | 47 +
plus/tool/pom.xml | 82 +
.../plus/tool/MainController.java | 324 ++++
.../plus/tool/PlusConfiguration.java | 54 +
.../plus/tool/WebMvcConfiguration.java | 90 +
.../exception/MissingSessionException.java | 39 +
.../src/main/resources/Messages.properties | 66 +
.../webapp/WEB-INF/templates/context.html | 62 +
.../webapp/WEB-INF/templates/contexts.html | 22 +
.../main/webapp/WEB-INF/templates/delete.html | 26 +
.../main/webapp/WEB-INF/templates/form.html | 117 ++
.../main/webapp/WEB-INF/templates/index.html | 27 +
.../webapp/WEB-INF/templates/notallow.html | 14 +
.../webapp/WEB-INF/templates/notfound.html | 14 +
.../main/webapp/WEB-INF/templates/tenant.html | 53 +
.../main/webapp/WEB-INF/tools/sakai.plus.xml | 11 +
plus/tool/src/main/webapp/WEB-INF/web.xml | 7 +
pom.xml | 2 +
portal/portal-impl/impl/pom.xml | 1 -
.../impl/src/bundle/sitenav.properties | 1 +
.../portal/charon/SkinnableCharonPortal.java | 7 +-
.../portal/charon/handlers/PlusHandler.java | 120 ++
.../portal/charon/handlers/SiteHandler.java | 13 +-
.../src/webapp/vm/morpheus/includePageBody.vm | 9 +-
.../impl/src/webapp/vm/morpheus/plus.vm | 360 ++++
portal/portal-service-impl/impl/pom.xml | 1 -
site-manage/site-manage-impl/impl/pom.xml | 1 -
.../tool/src/main/frontend/js/sakai-i18n.js | 7 +-
198 files changed, 12923 insertions(+), 633 deletions(-)
delete mode 100644 basiclti/basiclti-docs/resources/docs/LTI_CertPlan.xls
create mode 100644 basiclti/docs/CERTIFICATION.md
create mode 100644 basiclti/docs/CERTIFICATION_22.pdf
create mode 100644 basiclti/docs/CUSTOM.md
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/HACK/HackMoodle.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/HACK/README.md
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/deeplink/objects/ContentItem.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/deeplink/objects/DeepLinkResponse.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/deeplink/objects/LtiResourceLink.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/deeplink/objects/MiniLineItem.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/http/HttpClientUtil.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/lti13/LTI13AccessTokenUtil.java
rename basiclti/tsugi-util/src/java/org/tsugi/lti13/objects/{LTIPlatformMessage.java => LTILaunchMessage.java} (87%)
rename basiclti/tsugi-util/src/java/org/tsugi/lti13/objects/{ToolConfiguration.java => OpenIDClientRegistration.java} (75%)
rename basiclti/tsugi-util/src/java/org/tsugi/lti13/objects/{PlatformConfiguration.java => OpenIDProviderConfiguration.java} (93%)
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/nrps/objects/Container.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/nrps/objects/Member.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/nrps/objects/MemberMessage.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/oauth2/objects/ClientAssertion.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/provision/objects/RegistrationRequest.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/provision/objects/RegistrationResponse.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/shared/objects/DateRange.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/shared/objects/SizedUrl.java
create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/time/InstantUtil.java
create mode 100644 basiclti/tsugi-util/src/test/org/tsugi/HACK/MoodleHackTest.java
create mode 100644 basiclti/tsugi-util/src/test/org/tsugi/deeplink/DeepLinkResponseObjectTest.java
create mode 100644 basiclti/tsugi-util/src/test/org/tsugi/http/HttpUtilTest.java
create mode 100644 basiclti/tsugi-util/src/test/org/tsugi/lti13/LTI13AccessTokenUtilTest.java
create mode 100644 basiclti/tsugi-util/src/test/org/tsugi/nrps/NRPSTest.java
create mode 100644 basiclti/tsugi-util/src/test/org/tsugi/time/InstantUtilTest.java
create mode 100644 basiclti/tsugi-util/src/test/resources/deeplink/deep_link_settings.json
create mode 100644 basiclti/tsugi-util/src/test/resources/deeplink/sample_ltiresourcelink.json
create mode 100644 basiclti/tsugi-util/src/test/resources/deeplink/sample_response.json
create mode 100644 basiclti/tsugi-util/src/test/resources/nrps/sample_container.json
create mode 100644 basiclti/tsugi-util/src/test/resources/nrps/sample_member.json
create mode 100644 basiclti/tsugi-util/src/test/resources/nrps/sample_member_message.json
create mode 100644 basiclti/tsugi-util/src/test/resources/oauth2/sample_access_token.json
create mode 100644 library/src/morpheus-master/images/logo-jewel-square.png
create mode 100755 library/src/webapp/js/waterfall-light.js
create mode 100644 plus/README.md
create mode 100644 plus/SQL.md
create mode 100644 plus/TODO.md
create mode 100644 plus/api/pom.xml
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/Launch.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/PlusService.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/BaseLTI.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/Context.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/ContextLog.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/LineItem.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/Link.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/Membership.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/Score.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/Subject.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/model/Tenant.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/ContextLogRepository.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/ContextRepository.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/LineItemRepository.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/LinkRepository.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/MembershipRepository.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/ScoreRepository.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/SubjectRepository.java
create mode 100644 plus/api/src/main/java/org/sakaiproject/plus/api/repository/TenantRepository.java
create mode 100644 plus/docs/COMPLETED.md
create mode 100644 plus/docs/INSTALL-BLACKBOARD.md
create mode 100644 plus/docs/INSTALL-BRIGHTSPACE.md
create mode 100644 plus/docs/INSTALL-CANVAS.md
create mode 100644 plus/docs/INSTALL-MOODLE.md
create mode 100644 plus/docs/INSTALL-SAKAI.md
create mode 100644 plus/docs/NOTES.md
create mode 100644 plus/docs/TESTING.md
create mode 100644 plus/impl/pom.xml
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/PlusEventObserver.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/PlusServiceImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/jobs/SiteMembershipsSyncJob.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/ContextLogRepositoryImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/ContextRepositoryImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/LineItemRepositoryImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/LinkRepositoryImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/MembershipRepositoryImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/ScoreRepositoryImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/SubjectRepositoryImpl.java
create mode 100644 plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/TenantRepositoryImpl.java
create mode 100644 plus/impl/src/main/webapp/WEB-INF/components.xml
create mode 100644 plus/impl/src/test/org/sakaiproject/plus/impl/PlusModelTests.java
create mode 100644 plus/impl/src/test/org/sakaiproject/plus/impl/PlusServiceImplTests.java
create mode 100644 plus/impl/src/test/org/sakaiproject/plus/impl/PlusTestConfiguration.java
create mode 100644 plus/impl/src/test/resources/hibernate.properties
create mode 100644 plus/pom.xml
create mode 100644 plus/provider/maven.xml
create mode 100644 plus/provider/pom.xml
create mode 100644 plus/provider/src/bundle/plus.properties
create mode 100644 plus/provider/src/main/java/org/sakaiproject/plus/ProviderServlet.java
create mode 100644 plus/provider/src/main/webapp/WEB-INF/applicationContext.xml
create mode 100644 plus/provider/src/main/webapp/WEB-INF/descriptor.json
create mode 100644 plus/provider/src/main/webapp/WEB-INF/web.xml
create mode 100644 plus/provider/src/main/webapp/deeplink.jsp
create mode 100644 plus/provider/src/main/webapp/descriptor.txt
create mode 100644 plus/provider/src/main/webapp/whatisthis.htm
create mode 100644 plus/provider/src/test/org/sakaiproject/plus/provider/ProviderTests.java
create mode 100644 plus/tool/pom.xml
create mode 100644 plus/tool/src/main/java/org/sakaiproject/plus/tool/MainController.java
create mode 100644 plus/tool/src/main/java/org/sakaiproject/plus/tool/PlusConfiguration.java
create mode 100644 plus/tool/src/main/java/org/sakaiproject/plus/tool/WebMvcConfiguration.java
create mode 100644 plus/tool/src/main/java/org/sakaiproject/plus/tool/exception/MissingSessionException.java
create mode 100644 plus/tool/src/main/resources/Messages.properties
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/context.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/contexts.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/delete.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/form.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/index.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/notallow.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/notfound.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/templates/tenant.html
create mode 100644 plus/tool/src/main/webapp/WEB-INF/tools/sakai.plus.xml
create mode 100644 plus/tool/src/main/webapp/WEB-INF/web.xml
create mode 100644 portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/handlers/PlusHandler.java
create mode 100755 portal/portal-render-engine-impl/impl/src/webapp/vm/morpheus/plus.vm
diff --git a/assignment/impl/pom.xml b/assignment/impl/pom.xml
index 548750693dda..29a5310aab5f 100644
--- a/assignment/impl/pom.xml
+++ b/assignment/impl/pom.xml
@@ -219,7 +219,6 @@
org.sakaiproject.basiclti
basiclti-util
- ${sakai.version}
org.sakaiproject.basiclti
diff --git a/assignment/tool/pom.xml b/assignment/tool/pom.xml
index 16dd72ef4e87..ebb739a517d5 100644
--- a/assignment/tool/pom.xml
+++ b/assignment/tool/pom.xml
@@ -94,7 +94,6 @@
org.sakaiproject.basiclti
basiclti-api
- ${project.version}
org.sakaiproject.basiclti
@@ -104,7 +103,6 @@
org.sakaiproject.basiclti
basiclti-util
- ${project.version}
ant
diff --git a/basiclti/basiclti-blis/pom.xml b/basiclti/basiclti-blis/pom.xml
index c85084617157..d5469dbd3799 100644
--- a/basiclti/basiclti-blis/pom.xml
+++ b/basiclti/basiclti-blis/pom.xml
@@ -69,7 +69,6 @@
com.googlecode.json-simple
json-simple
- ${json.simple.version}
commons-lang
diff --git a/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java b/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java
index 21404d377dbf..9cbe9d27627c 100644
--- a/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java
+++ b/basiclti/basiclti-blis/src/java/org/sakaiproject/lti13/LTI13Servlet.java
@@ -87,14 +87,15 @@
import org.tsugi.lti13.LTI13ConstantsUtil;
import org.tsugi.oauth2.objects.AccessToken;
+import org.tsugi.oauth2.objects.ClientAssertion;
import org.tsugi.lti13.objects.Endpoint;
import org.tsugi.lti13.objects.LaunchLIS;
import org.tsugi.ags2.objects.Result;
import org.tsugi.ags2.objects.Score;
import org.tsugi.lti13.objects.LaunchJWT;
-import org.tsugi.lti13.objects.PlatformConfiguration;
+import org.tsugi.lti13.objects.OpenIDProviderConfiguration;
import org.tsugi.lti13.objects.LTIPlatformConfiguration;
-import org.tsugi.lti13.objects.LTIPlatformMessage;
+import org.tsugi.lti13.objects.LTILaunchMessage;
import org.sakaiproject.lti13.util.SakaiAccessToken;
import org.sakaiproject.lti13.util.SakaiLineItem;
@@ -323,7 +324,7 @@ protected void doPut(HttpServletRequest request, HttpServletResponse response) t
String[] parts = uri.split("/");
- // /imsblis/lti13/lineitems/{signed-placement}/{lineitem-id}
+ // /imsblis/lti13/lineitems/{signed-placement}
if (parts.length == 5 && "lineitem".equals(parts[3])) {
log.error("Attempt to modify on-demand line item request={}", uri);
LTI13Util.return400(response, "Attempt to modify an 'on-demand' line item");
@@ -360,6 +361,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
// Set score for auto-created line item
// /imsblis/lti13/lineitem/{signed-placement}
+ // SAK-47261 - This pattern with no lineItem does not work post SAK-47621
if (parts.length == 6 && "lineitem".equals(parts[3]) && "scores".equals(parts[5])) {
String signed_placement = parts[4];
String lineItem = null;
@@ -670,7 +672,7 @@ protected void handleSakaiConfig(HttpServletRequest request, HttpServletResponse
["RS256", "ES256"],
"claims_supported":
["sub", "iss", "name", "given_name", "family_name", "nickname", "picture", "email", "locale"],
- "https://purl.imsglobal.org/spec/lti-platform-configuration ": {
+ "https://purl.imsglobal.org/spec/lti-platform-configuration": {
"product_family_code": "ExampleLMS",
"messages_supported": [
{"type": "LtiResourceLinkRequest"},
@@ -715,18 +717,18 @@ protected void handleWellKnown(HttpServletRequest request, HttpServletResponse r
lpc.product_family_code = "sakailms.org";
lpc.version = sakaiVersion;
- LTIPlatformMessage mp = new LTIPlatformMessage();
+ LTILaunchMessage mp = new LTILaunchMessage();
mp.type = LaunchJWT.MESSAGE_TYPE_LAUNCH;
lpc.messages_supported.add(mp);
- mp = new LTIPlatformMessage();
+ mp = new LTILaunchMessage();
mp.type = LaunchJWT.MESSAGE_TYPE_DEEP_LINK;
lpc.messages_supported.add(mp);
lpc.variables.add(LTICustomVars.USER_ID);
lpc.variables.add(LTICustomVars.PERSON_EMAIL_PRIMARY);
- PlatformConfiguration pc = new PlatformConfiguration();
+ OpenIDProviderConfiguration pc = new OpenIDProviderConfiguration();
pc.issuer = issuerURL;
pc.authorization_endpoint = authOIDC;
pc.token_endpoint = tokenUrl;
@@ -796,9 +798,9 @@ protected void handleTokenPost(String tool_id, HttpServletRequest request, HttpS
return;
}
- String grant_type = request.getParameter(AccessToken.GRANT_TYPE);
- String client_assertion = request.getParameter(AccessToken.CLIENT_ASSERTION);
- String scope = request.getParameter(AccessToken.SCOPE);
+ String grant_type = request.getParameter(ClientAssertion.GRANT_TYPE);
+ String client_assertion = request.getParameter(ClientAssertion.CLIENT_ASSERTION);
+ String scope = request.getParameter(ClientAssertion.SCOPE);
String missing = "";
if (grant_type == null) {
missing += " " + "grant_type";
@@ -876,57 +878,57 @@ protected void handleTokenPost(String tool_id, HttpServletRequest request, HttpS
HashSet returnScopeSet = new HashSet ();
// Work through requested scopes
- if (scope.contains(Endpoint.SCOPE_LINEITEM_READONLY)) {
+ if (scope.contains(LTI13ConstantsUtil.SCOPE_LINEITEM_READONLY)) {
if (allowLineItems != 1) {
- LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_LINEITEM_READONLY);
+ LTI13Util.return400(response, "invalid_scope", LTI13ConstantsUtil.SCOPE_LINEITEM_READONLY);
log.error("Scope lineitem not allowed {}", tool_id);
return;
}
- returnScopeSet.add(Endpoint.SCOPE_LINEITEM_READONLY);
+ returnScopeSet.add(LTI13ConstantsUtil.SCOPE_LINEITEM_READONLY);
sat.addScope(SakaiAccessToken.SCOPE_LINEITEMS_READONLY);
}
- if (scope.contains(Endpoint.SCOPE_LINEITEM)) {
+ if (scope.contains(LTI13ConstantsUtil.SCOPE_LINEITEM)) {
if (allowLineItems != 1) {
- LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_LINEITEM);
+ LTI13Util.return400(response, "invalid_scope", LTI13ConstantsUtil.SCOPE_LINEITEM);
log.error("Scope lineitem not allowed {}", tool_id);
return;
}
- returnScopeSet.add(Endpoint.SCOPE_LINEITEM);
+ returnScopeSet.add(LTI13ConstantsUtil.SCOPE_LINEITEM);
sat.addScope(SakaiAccessToken.SCOPE_LINEITEMS);
sat.addScope(SakaiAccessToken.SCOPE_LINEITEMS_READONLY);
}
- if (scope.contains(Endpoint.SCOPE_SCORE)) {
+ if (scope.contains(LTI13ConstantsUtil.SCOPE_SCORE)) {
if (allowOutcomes != 1 || allowLineItems != 1) {
- LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_SCORE);
+ LTI13Util.return400(response, "invalid_scope", LTI13ConstantsUtil.SCOPE_SCORE);
log.error("Scope lineitem not allowed {}", tool_id);
return;
}
- returnScopeSet.add(Endpoint.SCOPE_SCORE);
+ returnScopeSet.add(LTI13ConstantsUtil.SCOPE_SCORE);
sat.addScope(SakaiAccessToken.SCOPE_BASICOUTCOME);
}
- if (scope.contains(Endpoint.SCOPE_RESULT_READONLY)) {
+ if (scope.contains(LTI13ConstantsUtil.SCOPE_RESULT_READONLY)) {
if (allowOutcomes != 1 || allowLineItems != 1) {
- LTI13Util.return400(response, "invalid_scope", Endpoint.SCOPE_RESULT_READONLY);
+ LTI13Util.return400(response, "invalid_scope", LTI13ConstantsUtil.SCOPE_RESULT_READONLY);
log.error("Scope lineitem not allowed {}", tool_id);
return;
}
- returnScopeSet.add(Endpoint.SCOPE_RESULT_READONLY);
+ returnScopeSet.add(LTI13ConstantsUtil.SCOPE_RESULT_READONLY);
sat.addScope(SakaiAccessToken.SCOPE_BASICOUTCOME);
}
- if (scope.contains(LaunchLIS.SCOPE_NAMES_AND_ROLES)) {
+ if (scope.contains(LTI13ConstantsUtil.SCOPE_NAMES_AND_ROLES)) {
if (allowRoster != 1) {
- LTI13Util.return400(response, "invalid_scope", LaunchLIS.SCOPE_NAMES_AND_ROLES);
+ LTI13Util.return400(response, "invalid_scope", LTI13ConstantsUtil.SCOPE_NAMES_AND_ROLES);
log.error("Scope lineitem not allowed {}", tool_id);
return;
}
- returnScopeSet.add(LaunchLIS.SCOPE_NAMES_AND_ROLES);
+ returnScopeSet.add(LTI13ConstantsUtil.SCOPE_NAMES_AND_ROLES);
sat.addScope(SakaiAccessToken.SCOPE_ROSTER);
}
@@ -1143,6 +1145,7 @@ protected void handleRegistrationEndpointPost(String tool_key_str, HttpServletRe
log.debug("jsonString={}", jsonString);
+ // TODO: Make this be a RegistrationRequest
Object js = JSONValue.parse(jsonString);
if (js == null || !(js instanceof JSONObject)) {
LTI13Util.return400(response, "Badly formatted JSON");
@@ -1193,6 +1196,7 @@ protected void handleRegistrationEndpointPost(String tool_key_str, HttpServletRe
jso.put("client_id", client_id);
+ // TODO: Make this be a RegistrationResponse
Object toolConfigurationObj = jso.get("https://purl.imsglobal.org/spec/lti-tool-configuration");
if ( toolConfigurationObj instanceof JSONObject ) {
JSONObject toolConfiguration = (JSONObject) toolConfigurationObj;
@@ -1937,23 +1941,14 @@ private void handleLineItemsUpdate(String signed_placement, String lineItem, Htt
return;
}
- // TODO: Does PUT need to return the entire line item - I think the code below
- // actually is wrong - we just need to do a GET to get the entire line
- // item after the PUT. It seems wasteful to always do the GET after PUT
- // when the tool can do it if it wants the newly updated item. So
- // For now I am sending nothing back for a pUT request.
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT
-
- /*
// Add the link to this lineitem
item.id = getOurServerUrl() + LTI13_PATH + "lineitems/" + signed_placement + "/" + retval.getId();
log.debug("Lineitem item={}",item);
- response.setContentType(LineItem.CONTENT_TYPE);
+ response.setContentType(SakaiLineItem.CONTENT_TYPE);
PrintWriter out = response.getWriter();
out.print(JacksonUtil.prettyPrint(item));
- */
}
/**
diff --git a/basiclti/basiclti-common/pom.xml b/basiclti/basiclti-common/pom.xml
index 82c16b259e31..b410bc8f1503 100644
--- a/basiclti/basiclti-common/pom.xml
+++ b/basiclti/basiclti-common/pom.xml
@@ -62,7 +62,14 @@
com.googlecode.json-simple
json-simple
- ${json.simple.version}
+
+
+ io.jsonwebtoken
+ jjwt-api
+
+
+ com.nimbusds
+ nimbus-jose-jwt
org.springframework
diff --git a/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiBLTIUtil.java b/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiBLTIUtil.java
index 9d6c4dbf645a..6f5727755442 100644
--- a/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiBLTIUtil.java
+++ b/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiBLTIUtil.java
@@ -1903,7 +1903,7 @@ public static String[] postLaunchJWT(Properties toolProps, Properties ltiProps,
) {
Endpoint endpoint = new Endpoint();
endpoint.scope = new ArrayList<>();
- endpoint.scope.add(Endpoint.SCOPE_LINEITEM);
+ endpoint.scope.add(LTI13ConstantsUtil.SCOPE_LINEITEM);
if ( allowOutcomes != 0 && outcomesEnabled() && content != null) {
SakaiLineItem defaultLineItem = LineItemUtil.getDefaultLineItem(site, content);
@@ -2048,9 +2048,18 @@ public static String[] postLaunchJWT(Properties toolProps, Properties ltiProps,
}
}
- Integer form_id = jws.hashCode();
+ String launch_error = rb.getString("error.submit.timeout")+" "+launch_url;
+ String html = getJwsHTMLForm(launch_url, "id_token", jws, ljs, state, launch_error, dodebug);
+
+ String[] retval = {html, launch_url};
+ return retval;
+ }
+
+ public static String getJwsHTMLForm(String launch_url, String form_field, String jwt, String jsonStr, String state, String launch_error, boolean dodebug) {
+
+ Integer form_id = jwt.hashCode();
String html = "
\n\n--- State:
"
+ BasicLTIUtil.htmlspecialchars(state)
+ "
\n\n--- Encoded JWT:
"
- + BasicLTIUtil.htmlspecialchars(jws)
+ + BasicLTIUtil.htmlspecialchars(jwt)
+ "
\n";
html += "\n";
}
- String[] retval = {html, launch_url};
- return retval;
+ return html;
}
public static String getSourceDID(User user, Placement placement, Properties config) {
@@ -2449,7 +2456,6 @@ private static Object handleGradebook(String sourcedid, HttpServletRequest reque
}
retval = retMap;
} else if (isDelete) {
- g.setAssignmentScoreString(siteId, gradebookColumn.getId(), user_id, null, "External Outcome");
g.deleteAssignmentScoreComment(siteId, gradebookColumn.getId(), user_id);
log.info("Delete Score site={} title={} user_id={}", siteId, title, user_id);
retval = Boolean.TRUE;
diff --git a/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiContentItemUtil.java b/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiContentItemUtil.java
index 1104c6ef8bcc..b25a9796cf1e 100644
--- a/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiContentItemUtil.java
+++ b/basiclti/basiclti-common/src/java/org/sakaiproject/basiclti/util/SakaiContentItemUtil.java
@@ -42,11 +42,11 @@ public static LtiLinkItem getLtiLinkItem(String toolRegistration)
Tool theTool = ToolManager.getTool(toolRegistration);
if ( theTool == null ) return null;
- Icon icon = new Icon("https://www.apereo.org/sites/all/themes/apereo/images/apereo-logo-white-bg.png");
- icon.setHeight(64);
- icon.setWidth(64);
+ Icon icon = new Icon("https://www.apereo.org/sites/all/themes/apereo/images/apereo-logo-white-bg.png");
+ icon.setHeight(64);
+ icon.setWidth(64);
- PlacementAdvice placementAdvice = new PlacementAdvice();
+ PlacementAdvice placementAdvice = new PlacementAdvice();
// If we are http, lets go in a new window
String serverUrl = SakaiBLTIUtil.getOurServerUrl();
@@ -56,14 +56,14 @@ public static LtiLinkItem getLtiLinkItem(String toolRegistration)
placementAdvice.setPresentationDocumentTarget(placementAdvice.WINDOW);
}
- LtiLinkItem item = new LtiLinkItem(toolRegistration, placementAdvice, icon);
- item.setUrl(SakaiLTIProviderUtil.getProviderLaunchUrl(toolRegistration));
- item.setTitle(theTool.getTitle());
+ LtiLinkItem item = new LtiLinkItem(toolRegistration, placementAdvice, icon);
+ item.setUrl(SakaiLTIProviderUtil.getProviderLaunchUrl(toolRegistration));
+ item.setTitle(theTool.getTitle());
// Because of weirdness in the CI Implementations and unclear semantics
// between text and title, we send title twice
- // item.setText(theTool.getDescription());
- item.setText(theTool.getTitle());
+ // item.setText(theTool.getDescription());
+ item.setText(theTool.getTitle());
return item;
}
@@ -73,9 +73,11 @@ public static ContentItemResponse getContentItemResponse(String toolRegistration
LtiLinkItem item = getLtiLinkItem(toolRegistration);
if ( item == null ) return null;
- ContentItemResponse resp = new ContentItemResponse();
- resp.addGraph(item);
+ ContentItemResponse resp = new ContentItemResponse();
+ resp.addGraph(item);
return resp;
}
+ // vim: tabstop=4 noet
+
}
diff --git a/basiclti/basiclti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java b/basiclti/basiclti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
index cdda0a9a5eb9..e60e944564e1 100644
--- a/basiclti/basiclti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
+++ b/basiclti/basiclti-common/src/java/org/sakaiproject/lti13/LineItemUtil.java
@@ -199,10 +199,14 @@ public static Assignment createLineItem(String context_id, Long tool_id, MapC!`}_U>Z}oW}z0b@&&%NiId(OG%
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
a!xL#<$Vw^LFTE-HH(7%JSMYyJ0{;yad*Yw~
diff --git a/basiclti/basiclti-impl/pom.xml b/basiclti/basiclti-impl/pom.xml
index f9845cd0a9aa..b928221ca3ab 100644
--- a/basiclti/basiclti-impl/pom.xml
+++ b/basiclti/basiclti-impl/pom.xml
@@ -88,7 +88,6 @@
com.googlecode.json-simple
json-simple
- ${json.simple.version}
com.fasterxml.jackson.core
diff --git a/basiclti/basiclti-impl/src/bundle/ltiservice.properties b/basiclti/basiclti-impl/src/bundle/ltiservice.properties
index 469843965818..529c69dd604f 100644
--- a/basiclti/basiclti-impl/src/bundle/ltiservice.properties
+++ b/basiclti/basiclti-impl/src/bundle/ltiservice.properties
@@ -73,8 +73,8 @@ bl_allowlineitems=Allow External Tool to create grade columns
bl_allowroster=Provide Roster to External Tool
bl_allowsettings_ext=Allow External Tool to store setting data
pl_header=Tools can generally accept direct LTI Launches or a Content-Item / Deep-Link Selection launches. It is not common, but some tools can handle both types of launch at one endpoint.
-bl_allowcontentitem=The tool URL can receive a Content-Item / Deep-Link launch
-bl_pl_launch=The tool URL can receive an LTI launch
+bl_allowcontentitem=The tool URL can receive a Deep-Link / Content-Item
+bl_pl_launch=The tool URL can receive an LTI Resource Link launch
bl_pl_linkselection=The tool can receive a Content-Item or Deep-Link launch
pl_placement=Indicate where these tools are placed in Sakai.
bl_pl_contenteditor=Allow the tool to be used from the rich text editor
diff --git a/basiclti/basiclti-impl/src/java/org/sakaiproject/basiclti/impl/BasicLTISecurityServiceImpl.java b/basiclti/basiclti-impl/src/java/org/sakaiproject/basiclti/impl/BasicLTISecurityServiceImpl.java
index db6df68b710c..a37b34f3ae31 100644
--- a/basiclti/basiclti-impl/src/java/org/sakaiproject/basiclti/impl/BasicLTISecurityServiceImpl.java
+++ b/basiclti/basiclti-impl/src/java/org/sakaiproject/basiclti/impl/BasicLTISecurityServiceImpl.java
@@ -243,33 +243,6 @@ public boolean parseEntityReference(String reference, Reference ref)
return false;
}
- private void sendHTMLPage(HttpServletResponse res, String body)
- {
- try
- {
- res.setContentType("text/html; charset=UTF-8");
- res.setCharacterEncoding("utf-8");
- res.addDateHeader("Expires", System.currentTimeMillis() - (1000L * 60L * 60L * 24L * 365L));
- res.addDateHeader("Last-Modified", System.currentTimeMillis());
- res.addHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0");
- res.addHeader("Pragma", "no-cache");
- java.io.PrintWriter out = res.getWriter();
-
- out.println("");
- out.println("");
- out.println("\n");
- out.println("");
- out.println("\n\n");
- out.println(body);
- out.println("\n\n");
- }
- catch (Exception e)
- {
- log.warn("Failed to send HTML page.", e);
- }
-
- }
-
private void doSplash(HttpServletRequest req, HttpServletResponse res, String splash, ResourceLoader rb)
{
// req.getRequestURL()=http://localhost:8080/access/lti/site/85fd092b-1755-4aa9-8abc-e6549527dce0/content:0
@@ -281,7 +254,7 @@ private void doSplash(HttpServletRequest req, HttpServletResponse res, String sp
body += rb.getString("launch.button", "Press to continue to proceed to external tool.");
body += "\">\n";
body += splash+"";
- sendHTMLPage(res, body);
+ org.tsugi.basiclti.BasicLTIUtil.sendHTMLPage(res, body);
}
// Do a redirect in HTML + JavaScript instead of with a 302 so we have some recovery options inside an iframe
@@ -303,7 +276,7 @@ private void doRedirect(HttpServletRequest req, HttpServletResponse res, String
body.append("window.location='"+redirectUrl+"';\n");
body.append("\n");
body.append("");
- sendHTMLPage(res, body.toString());
+ org.tsugi.basiclti.BasicLTIUtil.sendHTMLPage(res, body.toString());
}
/*
@@ -407,7 +380,7 @@ private boolean sanityCheck(HttpServletRequest req, HttpServletResponse res,
String oidc_endpoint = (String) tool.get(LTIService.LTI13_OIDC_ENDPOINT);
if (SakaiBLTIUtil.isLTI13(tool, content) && StringUtils.isBlank(oidc_endpoint) ) {
String errorMessage = "
" + SakaiBLTIUtil.getRB(rb, "error.no.oidc_endpoint", "Missing oidc_endpoint value for LTI 1.3 launch") + "
";
- sendHTMLPage(res, errorMessage);
+ org.tsugi.basiclti.BasicLTIUtil.sendHTMLPage(res, errorMessage);
return false;
}
@@ -679,7 +652,7 @@ else if (refId.startsWith("export:") && refId.length() > 7)
try
{
if (retval != null) {
- sendHTMLPage(res, retval[0]);
+ org.tsugi.basiclti.BasicLTIUtil.sendHTMLPage(res, retval[0]);
}
String refstring = ref.getReference();
if ( retval != null && retval.length > 1 ) refstring = retval[1];
diff --git a/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/LTIRoleMapperImpl.java b/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/LTIRoleMapperImpl.java
index 261a8230d63b..de2f8f137804 100644
--- a/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/LTIRoleMapperImpl.java
+++ b/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/LTIRoleMapperImpl.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://opensource.org/licenses/ecl2
+ * http://opensource.org/licenses/ecl2
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -33,132 +33,121 @@
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
+import lombok.Setter;
+
/**
* @author Adrian Fish
*/
@Slf4j
public class LTIRoleMapperImpl implements LTIRoleMapper {
- /**
- * Injected from Spring, see components.xml
- */
- private SiteService siteService = null;
- public void setSiteService(SiteService siteService) {
- this.siteService = siteService;
- }
-
- private ServerConfigurationService serverConfigurationService;
- public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) {
- this.serverConfigurationService = serverConfigurationService;
- }
-
- public Map.Entry mapLTIRole(Map payload, User user, Site site, boolean trustedConsumer) throws LTIException {
-
- // Check if the user is a member of the site already
- boolean userExistsInSite = false;
- try {
- Member member = site.getMember(user.getId());
- if (member != null && BasicLTIUtil.equals(member.getUserEid(), user.getEid())) {
- userExistsInSite = true;
- return new AbstractMap.SimpleImmutableEntry(userRole(payload), member.getRole().getId());
- }
- } catch (Exception e) {
- log.warn(e.getLocalizedMessage(), e);
- throw new LTIException( "launch.site.invalid", "siteId="+site.getId(), e);
- }
-
- if (log.isDebugEnabled()) {
- log.debug("userExistsInSite={}", userExistsInSite);
- }
-
- // If not a member of the site, and we are a trusted consumer, error
- // Otherwise, add them to the site
- if (!userExistsInSite && trustedConsumer) {
- throw new LTIException( "launch.site.user.missing", "user_id="+user.getId()+ ", siteId="+site.getId(), null);
- }
-
- String ltiRole = null;
-
- if (trustedConsumer) {
- // If the launch is from a trusted consumer, just return the user's
- // role in the site. No need to map.
- Member member = site.getMember(user.getId());
- return new AbstractMap.SimpleImmutableEntry(ltiRole, member.getRole().getId());
- } else {
- ltiRole = userRole(payload);
- }
-
- if (log.isDebugEnabled()) {
- log.debug("ltiRole={}", ltiRole);
- }
-
- try {
- site = siteService.getSite(site.getId());
- Set roles = site.getRoles();
-
- //BLTI-151 see if we can directly map the incoming role to the list of site roles
- String newRole = null;
- if (log.isDebugEnabled()) {
- log.debug("Incoming ltiRole: {}", ltiRole);
- }
- for (Role r : roles) {
- String roleId = r.getId();
-
- if (BasicLTIUtil.equalsIgnoreCase(roleId, ltiRole)) {
- newRole = roleId;
- if (log.isDebugEnabled()) {
- log.debug("Matched incoming role to role in site: {}", roleId);
- }
- break;
- }
- }
-
- //if we haven't mapped a role, check against the standard roles and fallback
- if (BasicLTIUtil.isBlank(newRole)) {
-
- if (log.isDebugEnabled()) {
- log.debug("No match, falling back to determine role");
- }
-
- String maintainRole = site.getMaintainRole();
-
- if (maintainRole == null) {
- maintainRole = serverConfigurationService.getString("lti.role.mapping.Instructor", null);
+ /**
+ * Injected from Spring, see components.xml
+ */
+ @Setter private SiteService siteService;
+
+ @Setter private ServerConfigurationService serverConfigurationService;
+
+ public Map.Entry mapLTIRole(Map payload, User user, Site site, boolean trustedConsumer) throws LTIException {
+
+ // Check if the user is a member of the site already
+ boolean userExistsInSite = false;
+ try {
+ Member member = site.getMember(user.getId());
+ if (member != null && BasicLTIUtil.equals(member.getUserEid(), user.getEid())) {
+ userExistsInSite = true;
+ return new AbstractMap.SimpleImmutableEntry(userRole(payload), member.getRole().getId());
+ }
+ } catch (Exception e) {
+ log.warn(e.getLocalizedMessage(), e);
+ throw new LTIException( "launch.site.invalid", "siteId="+site.getId(), e);
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("userExistsInSite={}", userExistsInSite);
+ }
+
+ // If not a member of the site, and we are a trusted consumer, error
+ // Otherwise, add them to the site
+ if (!userExistsInSite && trustedConsumer) {
+ throw new LTIException( "launch.site.user.missing", "user_id="+user.getId()+ ", siteId="+site.getId(), null);
+ }
+
+ String ltiRole = null;
+
+ if (trustedConsumer) {
+ // If the launch is from a trusted consumer, just return the user's
+ // role in the site. No need to map.
+ Member member = site.getMember(user.getId());
+ return new AbstractMap.SimpleImmutableEntry(ltiRole, member.getRole().getId());
+ } else {
+ ltiRole = userRole(payload);
}
- boolean isInstructor = ltiRole.indexOf("instructor") >= 0;
- if (isInstructor && maintainRole != null) {
- newRole = maintainRole;
- }else{
- newRole=serverConfigurationService.getString("lti.role.mapping.Student", null);
+ log.debug("ltiRole={}", ltiRole);
+
+ try {
+ site = siteService.getSite(site.getId());
+ Set roles = site.getRoles();
+
+ //BLTI-151 see if we can directly map the incoming role to the list of site roles
+ String newRole = null;
+ log.debug("Incoming ltiRole: {}", ltiRole);
+ for (Role r : roles) {
+ String roleId = r.getId();
+
+ if (BasicLTIUtil.equalsIgnoreCase(roleId, ltiRole)) {
+ newRole = roleId;
+ log.debug("Matched incoming role to role in site: {}", roleId);
+ break;
+ }
+ }
+
+ //if we haven't mapped a role, check against the standard roles and fallback
+ if (BasicLTIUtil.isBlank(newRole)) {
+
+ log.debug("No match, falling back to determine role");
+
+ String maintainRole = site.getMaintainRole();
+
+ if (maintainRole == null) {
+ maintainRole = serverConfigurationService.getString("lti.role.mapping.Instructor", null);
+ }
+
+ boolean isInstructor = ltiRole.indexOf("instructor") >= 0;
+ if (isInstructor && maintainRole != null) {
+ newRole = maintainRole;
+ } else {
+ newRole=serverConfigurationService.getString("lti.role.mapping.Student", null);
+ }
+
+ log.debug("Determined newRole as: {}", newRole);
+ }
+
+ if (newRole == null) {
+ log.warn("Could not find Sakai role, role={} user={} site={}", ltiRole, user.getId(), site.getId());
+ throw new LTIException( "launch.role.missing", "siteId="+site.getId(), null);
+
+ }
+
+ return new AbstractMap.SimpleImmutableEntry(ltiRole, newRole);
+ } catch (Exception e) {
+ log.warn("Could not map role role={} user={} site={}", ltiRole, user.getId(), site.getId());
+ log.warn(e.getLocalizedMessage(), e);
+ throw new LTIException( "map.role", "siteId="+site.getId(), e);
}
-
-
- if (log.isDebugEnabled()) {
- log.debug("Determined newRole as: {}", newRole);
- }
- }
- if (newRole == null) {
- log.warn("Could not find Sakai role, role={} user={} site={}", ltiRole, user.getId(), site.getId());
- throw new LTIException( "launch.role.missing", "siteId="+site.getId(), null);
-
- }
-
- return new AbstractMap.SimpleImmutableEntry(ltiRole, newRole);
- } catch (Exception e) {
- log.warn("Could not map role role={} user={} site={}", ltiRole, user.getId(), site.getId());
- log.warn(e.getLocalizedMessage(), e);
- throw new LTIException( "map.role", "siteId="+site.getId(), e);
- }
- }
+ }
private String userRole(Map payload) {
String ltiRole;
ltiRole = (String) payload.get(BasicLTIConstants.ROLES);
if (ltiRole == null) {
- ltiRole = "";
+ ltiRole = "";
} else {
- ltiRole = ltiRole.toLowerCase();
+ ltiRole = ltiRole.toLowerCase();
}
return ltiRole;
}
+
+ // vim: tabstop=4 noet
+
}
diff --git a/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/UserFinderOrCreatorImpl.java b/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/UserFinderOrCreatorImpl.java
index bf1b3a11b061..17f3bf83217e 100644
--- a/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/UserFinderOrCreatorImpl.java
+++ b/basiclti/basiclti-impl/src/java/org/sakaiproject/lti/impl/UserFinderOrCreatorImpl.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.opensource.org/licenses/ECL-2.0
+ * http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,6 +19,8 @@
import java.util.Collection;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+
import lombok.extern.slf4j.Slf4j;
import org.tsugi.basiclti.BasicLTIConstants;
@@ -31,114 +33,127 @@
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.user.api.UserDirectoryService;
+import lombok.Setter;
+
/**
* @author Adrian Fish
*/
@Slf4j
public class UserFinderOrCreatorImpl implements UserFinderOrCreator {
- private UserDirectoryService userDirectoryService = null;
- public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
- this.userDirectoryService = userDirectoryService;
- }
-
- public User findOrCreateUser(Map payload, boolean trustedConsumer, boolean emailtrusted) throws LTIException {
-
- User user;
- String eid=null;
- String user_id = (String) payload.get(BasicLTIConstants.USER_ID);
-
- // Get the eid, either from the value provided or if trusted get it from the user_id,otherwise construct it.
- if(!emailtrusted){
- eid = getEid(payload, trustedConsumer, user_id);
- }
-
-
- // If we did not get first and last name, split lis_person_name_full
- final String fullname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_FULL);
- String fname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_GIVEN);
- String lname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_FAMILY);
- String email = (String) payload.get(BasicLTIConstants.LIS_PERSON_CONTACT_EMAIL_PRIMARY);
-
- if (fname == null && lname == null && fullname != null) {
- int ipos = fullname.trim().lastIndexOf(' ');
- if (ipos == -1) {
- fname = fullname;
- } else {
- fname = fullname.substring(0, ipos);
- lname = fullname.substring(ipos + 1);
- }
- }
-
- // If trusted consumer, login, if email trusted consumer then we look up the user info based on the email address otherwise check for existing user and create one if required
- // Note that if trusted, then the user must have already logged into Sakai in order to have an account stub created for them
- // otherwise this will fail since they don't exist. Perhaps this should be addressed?
- if (trustedConsumer) {
- try {
- if (BasicLTIUtil.isNotBlank((String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID))) {
- user = userDirectoryService.getUserByEid(eid);
- } else {
- user = userDirectoryService.getUser(user_id);
- }
- } catch (UserNotDefinedException e) {
- throw new LTIException("launch.user.invalid", "user_id=" + user_id, e);
- }
- return user;
- }
+ @Setter private UserDirectoryService userDirectoryService;
+
+ public User findOrCreateUser(Map payload, boolean trustedConsumer, boolean emailtrusted) throws LTIException {
+
+ User user;
+ String eid=null;
+ String user_id = (String) payload.get(BasicLTIConstants.USER_ID);
+
+ // Get the eid, either from the value provided or if trusted get it from the user_id, otherwise construct it.
+ if(!emailtrusted){
+ eid = getEid(payload, trustedConsumer, user_id);
+ }
+
+ // If we did not get first and last name, split lis_person_name_full
+ final String fullname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_FULL);
+ String fname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_GIVEN);
+ String lname = (String) payload.get(BasicLTIConstants.LIS_PERSON_NAME_FAMILY);
+ String email = (String) payload.get(BasicLTIConstants.LIS_PERSON_CONTACT_EMAIL_PRIMARY);
+ String subject_guid = (String) payload.get("subject_guid");
+ if (emailtrusted && StringUtils.isEmpty(email)) {
+ log.warn("trusting email as eid, no email provided subject_guid={}", subject_guid);
+ eid = subject_guid;
+ }
+
+ if (fname == null && lname == null && fullname != null) {
+ int ipos = fullname.trim().lastIndexOf(' ');
+ if (ipos == -1) {
+ fname = fullname;
+ } else {
+ fname = fullname.substring(0, ipos);
+ lname = fullname.substring(ipos + 1);
+ }
+ }
+
+ // If trusted consumer, login, if email trusted consumer then we look up the user info based on the email address otherwise check for existing user and create one if required
+ // Note that if trusted, then the user must have already logged into Sakai in order to have an account stub created for them
+ // otherwise this will fail since they don't exist. Perhaps this should be addressed?
+ if (trustedConsumer) {
+ try {
+ if (BasicLTIUtil.isNotBlank((String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID))) {
+ user = userDirectoryService.getUserByEid(eid);
+ } else {
+ user = userDirectoryService.getUser(user_id);
+ }
+ } catch (UserNotDefinedException e) {
+ throw new LTIException("launch.user.invalid", "user_id=" + user_id, e);
+ }
+ return user;
+ }
+
/*
* looking up user based on email address may return multiple results,
* this is not a valid case hence this is an error condition with this
* work flow
*/
- if (emailtrusted) {
- Collection findUsersByEmail = userDirectoryService.findUsersByEmail((String) email);
- if (!findUsersByEmail.isEmpty()) {
- if (findUsersByEmail.size() > 1) {
- log.warn("multiple user id's exist for emailaddress= {}", email);
- throw new LTIException("launch.user.multiple.emailaddress", "email=" + email,null);
- }
- user = (User) findUsersByEmail.toArray()[0];
- } else {
- log.warn("Invalid user for emailaddress= {}", email);
- throw new LTIException("launch.user.invalid", "email=" + email,null);
- }
- return user;
- }
-
- try {
- user = userDirectoryService.getUserByEid(eid);
- } catch (Exception e) {
- if (log.isDebugEnabled()) {
- log.debug(e.getLocalizedMessage(), e);
- }
- user = null;
- }
-
- if (user == null) {
- try {
- String hiddenPW = IdManager.createUuid();
- userDirectoryService.addUser(null, eid, fname, lname, email, hiddenPW, "registered", null);
- log.info("Created user={}", eid);
- user = userDirectoryService.getUserByEid(eid);
- } catch (Exception e) {
- throw new LTIException("launch.create.user", "user_id=" + user_id, e);
- }
- }
-
- // post the login event
- // eventTrackingService().post(eventTrackingService().newEvent(EVENT_LOGIN,
- // null, true));
-
- return user;
- }
-
- private String getEid(Map payload, boolean trustedConsumer, String user_id) throws LTIException {
-
- String eid;
- String oauth_consumer_key = (String) payload.get("oauth_consumer_key");
- String ext_sakai_provider_eid = (String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID);
-
- if (BasicLTIUtil.isNotBlank(ext_sakai_provider_eid)){
+ if (emailtrusted && ! StringUtils.isEmpty(email)) {
+ Collection findUsersByEmail = userDirectoryService.findUsersByEmail((String) email);
+ if (findUsersByEmail.isEmpty()) {
+ eid = email;
+ log.warn("creating new user with eid based on email={}", eid);
+ } else {
+ if (findUsersByEmail.size() > 1) {
+ log.warn("multiple user id's exist for emailaddress= {}", email);
+ throw new LTIException("launch.user.multiple.emailaddress", "email=" + email,null);
+ }
+ user = (User) findUsersByEmail.toArray()[0];
+ return user;
+ }
+ }
+
+ if ( StringUtils.isEmpty(eid)) {
+ throw new LTIException("launch.user.noeid", "user_id=" + user_id);
+ }
+
+
+ try {
+ user = userDirectoryService.getUserByEid(eid);
+ } catch (Exception e) {
+ log.warn("Could not find user: {}: {}", eid, e.toString());
+ user = null;
+ }
+
+ /*
+ * Create the user with an unguessable password. In the future, perhaps with SSO or perhaps
+ * with password reset enabled, and if the eid is the email address, this user will be
+ * able to log into their account directly.
+ */
+ if (user == null) {
+ try {
+ String hiddenPW = IdManager.createUuid();
+ userDirectoryService.addUser(null, eid, fname, lname, email, hiddenPW, "registered", null);
+ log.info("Created user={}", eid);
+ user = userDirectoryService.getUserByEid(eid);
+ } catch (Exception e) {
+ throw new LTIException("launch.create.user", "user_id=" + user_id, e);
+ }
+ }
+
+ // post the login event
+ // eventTrackingService().post(eventTrackingService().newEvent(EVENT_LOGIN,
+ // null, true));
+
+ return user;
+ }
+
+ private String getEid(Map payload, boolean trustedConsumer, String user_id) throws LTIException {
+
+ String eid;
+ String oauth_consumer_key = (String) payload.get("oauth_consumer_key");
+ String subject_guid = (String) payload.get("subject_guid");
+ String ext_sakai_provider_eid = (String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID);
+
+ if (BasicLTIUtil.isNotBlank(ext_sakai_provider_eid)){
eid = (String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID);
} else {
@@ -149,6 +164,8 @@ private String getEid(Map payload, boolean trustedConsumer, String user_id) thro
log.error(e.getLocalizedMessage(), e);
throw new LTIException( "launch.user.invalid", "user_id="+user_id, e);
}
+ } else if ( BasicLTIUtil.isNotBlank(subject_guid) ) {
+ eid = "subject:" + subject_guid;
} else {
eid = oauth_consumer_key + ":" + user_id;
}
@@ -156,6 +173,8 @@ private String getEid(Map payload, boolean trustedConsumer, String user_id) thro
log.debug("eid={}", eid);
}
}
- return eid;
- }
+ return eid;
+ }
+
+
}
diff --git a/basiclti/basiclti-portlet/src/bundle/basiclti.properties b/basiclti/basiclti-portlet/src/bundle/basiclti.properties
index 910f4d14b1d6..7e38b0479193 100644
--- a/basiclti/basiclti-portlet/src/bundle/basiclti.properties
+++ b/basiclti/basiclti-portlet/src/bundle/basiclti.properties
@@ -85,6 +85,7 @@ launch.tool.search = Error searching through tools
launch.tool.add = Could not add tool to site
launch.continue = Press to continue to tool
launch.user.invalid = Unable to find user
+launch.user.noeid = Unable to determine eid
launch.site.invalid = Unable to find site
launch.site.user.missing = Unable to find user in site
launch.site.tool.missing = Unable to find tool in site
diff --git a/basiclti/basiclti-tool/pom.xml b/basiclti/basiclti-tool/pom.xml
index 9a32141cfcc3..4f9c07ac53ad 100644
--- a/basiclti/basiclti-tool/pom.xml
+++ b/basiclti/basiclti-tool/pom.xml
@@ -20,7 +20,10 @@
${project.groupId}
basiclti-api
- ${project.version}
+
+
+ org.sakaiproject.basiclti
+ basiclti-util
${project.groupId}
diff --git a/basiclti/basiclti-tool/src/webapp/vm/lti_tool_insert.vm b/basiclti/basiclti-tool/src/webapp/vm/lti_tool_insert.vm
index 9cf266fafbd7..0f5d374d2b33 100644
--- a/basiclti/basiclti-tool/src/webapp/vm/lti_tool_insert.vm
+++ b/basiclti/basiclti-tool/src/webapp/vm/lti_tool_insert.vm
@@ -148,6 +148,7 @@ function importLTI13Config() {
jQuery.getJSON( importUrl, function(data) {
console.log(data);
+ jQuery("#lti13_on-input").prop('checked', true);
if ( data.initiate_login_uri ) jQuery("#lti13_oidc_endpoint").val(data.initiate_login_uri);
if ( data.jwks_uri ) jQuery("#lti13_tool_keyset").val(data.jwks_uri);
if ( data.client_id ) jQuery("#lti13_client_id").val(data.client_id);
diff --git a/basiclti/docs/CERTIFICATION.md b/basiclti/docs/CERTIFICATION.md
new file mode 100644
index 000000000000..cd46a6ffb781
--- /dev/null
+++ b/basiclti/docs/CERTIFICATION.md
@@ -0,0 +1,32 @@
+
+Certification Notes
+-------------------
+
+The certification suite used two different redirect urls - one for regular launch
+and one for deep link launches.
+
+ https://ltiadvantagevalidator.imsglobal.org/ltiplatform/deeplinkredirecturl.html
+ https://ltiadvantagevalidator.imsglobal.org/ltiplatform/oidcredirecturl.html
+
+When setting up a Sakai, you need to include both of these in the "LTI 1.3 Tool Redirect
+Endpoint(s) (comma separated and provided by the tool)" field separated by commas so that
+both the Deep Link and Resource Link launches work. The IMS OIDC login step chooses the
+right redirect url.
+
+ https://ltiadvantagevalidator.imsglobal.org/ltiplatform/deeplinkredirecturl.html, https://ltiadvantagevalidator.imsglobal.org/ltiplatform/oidcredirecturl.html
+
+Create two sites, with the same instructor. Use DeepLink to install a link in both sites
+before you do the Deep Link certification - it will use the second one - but you will need
+two links in two contexts with two users to pass the AGS part of the course or you will
+get a duplicate lineitem error.
+
+If you restart the test delete all of the gradebook columns before restarting or you
+will get duplicate columns in the AGS phase.
+
+It seems as though you can run the whole test with PII turned off.
+
+Also delete the LTI links installed by the DeepLink process if you restart.
+
+Please Submit a 2nd, Different Context Student Payload
+
+
diff --git a/basiclti/docs/CERTIFICATION_22.pdf b/basiclti/docs/CERTIFICATION_22.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..1587ad5f4b78453aceab2352ba5ce3c95f2e1dee
GIT binary patch
literal 756995
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