Skip to content

Commit a5bd71d

Browse files
csevadrianfish
andauthored
SAK-48101 - Plus: TA comes across with access perms and is listed as a student in the GB (sakaiproject#11064)
* SAK-48101 - Plus: TA comes across with access perms and is listed as a student in the GB Also incorporates: SAK-48087 Add the ability to choose site template and LTI role mapping for each tenant on incoming launches SAK-48117 Plus: Add a short description of plus tool Co-authored-by: Adrian Fish <[email protected]>
1 parent 802ca2e commit a5bd71d

File tree

17 files changed

+406
-178
lines changed

17 files changed

+406
-178
lines changed

basiclti/docs/LTIROLES.md

+17-18
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,6 @@ and the roles that Sakai provides via its Names and Roles Provisioning Service.
5454
When we reference "inbound" roles, we are referring to roles that are provided to
5555
Sakai when it is acting as an LTI provider.
5656

57-
Per-Tool Outbound Role Mapping
58-
------------------------------
59-
60-
There are special situations where a particular tool needs some very particular role
61-
mapping or your Sakai sites have additional roles beyond the out-of-the-box roles.
62-
When you are installing an LTI tool into Sakai, you have an option for mapping particular
63-
Sakai role to a particular LTI role using a set of mapping strings that specify a Sakai
64-
role and the corresponding LTI role:
65-
66-
Teaching Assistant:Instructor;Librarian:Learner
67-
68-
These examples are using the LTI 1.1 "short form" for the LTI roles.
69-
But you can also specify the full length roles as well, and you can specify more than one LTI
70-
role be the result of the role mapping. (blanks and linebreaks added for readablility):
71-
72-
Instructor:Instructor,http://purl.imsglobal.org/vocab/lis/v2/institution/person#Faculty;
73-
Teaching Assistant:Learner,http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper
74-
7557
Outbound LTI Role Mapping
7658
-------------------------
7759

@@ -134,6 +116,23 @@ cases or new roles arise in LTI and see common use, we can add them to the defau
134116
over time. The overrides allow for a quick response when it might take a little while before
135117
we can fix Sakai and you get an upgraded version.
136118

119+
Per-Tool Outbound Role Mapping
120+
------------------------------
121+
122+
There are special situations where a particular tool needs some very particular role
123+
mapping or your Sakai sites have additional roles beyond the out-of-the-box roles.
124+
When you are installing an LTI tool into Sakai, you have an option for mapping a particular
125+
Sakai role to a particular LTI role using a set of mapping strings, like this:
126+
127+
Teaching Assistant:Instructor;Librarian:Learner
128+
129+
These examples are using the LTI 1.1 "short form" for the LTI roles.
130+
But you can also specify the full length roles as well, and you can specify more than one LTI
131+
role be the result of the role mapping. (blanks and linebreaks added for readablility):
132+
133+
Instructor:Instructor,http://purl.imsglobal.org/vocab/lis/v2/institution/person#Faculty;
134+
Teaching Assistant:Learner,http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper
135+
137136
Inbound LTI Role Mapping
138137
------------------------
139138

basiclti/tsugi-util/src/java/org/tsugi/lti13/objects/LaunchJWT.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public class LaunchJWT extends BaseJWT {
9191

9292
// This is in LaunchJWTs
9393
@JsonProperty("nonce")
94-
public String nonce;
94+
public String nonce;
9595

9696
// Constructor
9797
public LaunchJWT() {
@@ -133,4 +133,16 @@ public boolean isInstructor() {
133133
return roles.contains(ROLE_INSTRUCTOR);
134134
}
135135

136+
@JsonIgnore
137+
public String getLTI11Roles() {
138+
if ( roles == null ) return null;
139+
140+
StringBuilder roleStr = new StringBuilder();
141+
for (String role : roles) {
142+
if ( roleStr.length() > 0 ) roleStr.append(',');
143+
roleStr.append(role);
144+
}
145+
return roleStr.toString();
146+
}
147+
136148
}

config/configuration/bundles/src/bundle/org/sakaiproject/config/bundle/default.sakai.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -5835,7 +5835,7 @@ rubrics.integration.token-secret=12345678900909091234
58355835
# Specify the realm id to be used to copy when making a new site if the plus.new.site.template
58365836
# does not have a realm in response to the first Plus launch to a site or tool.
58375837
# plus.new.site.realm
5838-
# DEFAULT: !site.template
5838+
# DEFAULT: !site.template.lti
58395839

58405840
# Specify the default site type to be created when making a new site in response to the
58415841
# first Plus launch to a site or tool. This is a fallback value if Sakai Plus cannot

plus/README.md

+91-5
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ running at least Sakai 23.
5252

5353
https://trunk-mysql.nightly.sakaiproject.org
5454

55-
Of course this server is reset every 24 hours so anything that you set up will vanish - and if
56-
you messJ anything up - your mistakes will convienently disappear. If you want access to a server
55+
Of course nightly servers are reset every 24 hours so anything that you set up will vanish - and if
56+
you mess anything up - your mistakes will convienently disappear. If you want access to a server
5757
for testing that will not be reset each evening, send an email to `plus at sakailms.org`.
5858

5959
Enabling SakaiPlus in sakai.properties
@@ -62,9 +62,6 @@ Enabling SakaiPlus in sakai.properties
6262
# Needed for launches to work inside iframes
6363
sakai.cookieSameSite=none
6464

65-
lti.role.mapping.Instructor=maintain
66-
lti.role.mapping.Student=access
67-
6865
# Not enabled by default
6966
plus.provider.enabled=true
7067
plus.tools.allowed=sakai.resources:sakai.site
@@ -118,3 +115,92 @@ for different kinds of launches.
118115
It might be necessary to install SakaiPlus more than once so that it shows up in all the right placements
119116
in a particular LMS.
120117

118+
SakaiPlus Tenants
119+
-----------------
120+
121+
A SakaiPlus server can support many "tenants". Each Learning System that you are plugging SakaiPlus into
122+
should have its own tenant. In SakaiPlus, all data within a tenant is isolated (each tenant is a 'silo').
123+
This way you can have a multi-tenant SakaiPlus server to serve many different learning systems. However
124+
it is also a quite typical use case to have one Enterprise LMS - say Canvas and one SakaiPlus server
125+
for the same school and to have a single Tenant entry in SakaiPlus for the Canvas system.
126+
127+
You can create a "draft" tenant with a Title and Issuer and optionally a Registration Lock. Once you have created
128+
a draft tenant, you can view the tenant to either start the LTI Dynamic Registration process or provide
129+
tool configuration to your calling learning system.
130+
131+
You can view the documentation for LTI Dynamic Registration at:
132+
133+
* [Dynamic Registration](https://www.imsglobal.org/spec/lti-dr/v1p0)
134+
135+
Each tenant in SakaiPlus has a set of data:
136+
137+
*Issuer*
138+
139+
Issuer is different for each LMS, but it is usually a URL like "https://plus.sakalms.org" - with no trailing slash.
140+
Sometimes this will be the domain where the LMS is hosted. For some cloud-hosted providers, they use one
141+
issuer across all customers. This field is required.
142+
143+
*Client ID*
144+
145+
These are provided by the launching Learning system as part of tool registration. If the Learning system
146+
supports LTI Dynamic Registration it will automatically populate this field.
147+
148+
*Deployment ID*
149+
150+
`Deployment ID` can be tricky. For some systems it is the same for an entire system and is provided
151+
as part of Dynamic Registration. For other systems a new `Deployent ID` is generated by each course.
152+
You can set the `Deployment ID` to `*` if you can accept any `Deployment ID` for a particular
153+
`Client ID`. See the per-LMS installation instructions above for details.
154+
155+
*Allowed Tools*
156+
157+
This field is a colon-separated list of Sakai tool ids like "sakai.resources". There is a special
158+
"sakai.site" tool id which controls the availability of the "entire site" launch". A simple default
159+
for this is "sakai.site" or "sakai.site:sakai.resources: ...". This field is required.
160+
161+
*New Window Tools*
162+
163+
This field is a colon-separated list of Sakai tool ids which will be forced to always open in a
164+
new window. The "sakai.site" is always launched in a new window. This is typically left blank unless
165+
it is known that a particular tool just does not work well in an iframe. Or perhaps you are setting
166+
up a single tool server and want it to always be in a new window.
167+
168+
*Trust Email*
169+
170+
If the Learning system that is calling SakaiPlus for this tenant sends email, you *should* trust the
171+
email address to avoid creating multiple user records for each user in each site. Of you mark this tenant
172+
as 'trust email', and the calling system provides the email address of the user, multiple launches from
173+
multiple contexts will all be linked to the same user within this Tenant in SakaiPlus.
174+
175+
*Site Template*
176+
177+
This specifies an existing site like `!plussite` which will be copied to make new site when SakaiPlus
178+
receives an incoming site. This template site determines the defaoly tools that are added to the new
179+
SakaiPlus site. The default is '!plussite' unless it is changed using the `plus.new.site.template` Sakai property.
180+
181+
182+
*Realm Template*
183+
184+
This specifies an existing realm like `!site.template.lti` which will be copied to set the roles and
185+
permissions used when creating a new site when SakaiPlus receives an incoming site. The default is
186+
`!site.template.lti` unless it is changed using the `plus.new.site.realm` Sakai property.
187+
188+
*Inbound Role Map*
189+
190+
This field allows for overriding the default mapping from incoming LTI roles to Sakai roles. See
191+
this documentation for detail on how role mapping works and the format for role mapping entries.
192+
193+
* [Sakai to LTI Role Mapping](../basiclti/docs/LTIROLES.md)
194+
195+
*Registration Lock*
196+
197+
You set this field to "unlock" LTI Dynamic Registration for this tenant. It should only be set while
198+
performing dynamic registration and should be cleared after dynamic registration is complete.
199+
If the launching system does not support dynamic registration you will set these manually.
200+
201+
The `LMS Keyset Url`, `LMS Authorization Url`, `LMS Token Url`, and `LMS Token Audience` fields are
202+
set up as part of tool registration with the calling learning system. If the system supports
203+
LTI Dynamic Registration these values should be set automatically. The `LMS Token Audience`
204+
is left blank for most systems except for Desire2Learn.
205+
206+

plus/TODO.md

-40
This file was deleted.

plus/api/src/main/java/org/sakaiproject/plus/api/PlusService.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public interface PlusService {
5454
public static final String PLUS_NEW_SITE_TEMPLATE_DEFAULT = "!plussite";
5555
public static final String PLUS_NEW_SITE_TEMPLATE_BACKUP = "!worksite";
5656
public static final String PLUS_NEW_SITE_REALM = "plus.new.site.realm";
57-
public static final String PLUS_NEW_SITE_REALM_DEFAULT = "!site.template";
57+
public static final String PLUS_NEW_SITE_REALM_DEFAULT = "!site.template.lti";
5858
public static final String PLUS_NEW_SITE_TYPE = "plus.new.site.type";
5959
public static final String PLUS_NEW_SITE_TYPE_DEFAULT = "project";
6060

plus/api/src/main/java/org/sakaiproject/plus/api/model/Membership.java

+14-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
import javax.persistence.JoinColumn;
2727
import javax.persistence.ManyToOne;
2828
import javax.persistence.Table;
29+
import javax.persistence.Lob;
30+
31+
import org.apache.commons.lang3.StringUtils;
2932

3033
import org.sakaiproject.springframework.data.PersistableEntity;
3134

@@ -41,9 +44,6 @@
4144
@Data
4245
public class Membership extends BaseLTI implements PersistableEntity<Long> {
4346

44-
public static final Integer ROLE_LEARNER = 0;
45-
public static final Integer ROLE_INSTRUCTOR = 1000;
46-
4747
@Id @GeneratedValue
4848
@Column(name = "MEMBERSHIP_ID")
4949
private Long id;
@@ -60,9 +60,16 @@ public class Membership extends BaseLTI implements PersistableEntity<Long> {
6060
@ToString.Exclude
6161
private Context context;
6262

63-
@Column(name = "ROLE", nullable = true)
64-
private Integer role;
63+
@Lob
64+
@Column(name = "LTI_ROLES", nullable = true)
65+
private String ltiRoles;
66+
67+
@Lob
68+
@Column(name = "LTI_ROLES_OVERRIDE", nullable = true)
69+
private String ltiRolesOverride;
6570

66-
@Column(name = "ROLE_OVERRIDE", nullable = true)
67-
private Integer roleOverride;
71+
public boolean isInstructor() {
72+
if ( ltiRoles == null ) return false;
73+
return StringUtils.containsIgnoreCase(ltiRoles, "instructor") || StringUtils.containsIgnoreCase(ltiRolesOverride, "instructor");
74+
}
6875
}

plus/api/src/main/java/org/sakaiproject/plus/api/model/Tenant.java

+10
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ public class Tenant extends BaseLTI implements PersistableEntity<String> {
9191
@Column(name = "VERBOSE")
9292
private Boolean verbose = Boolean.FALSE;
9393

94+
@Column(name = "SITE_TEMPLATE", length = LENGTH_SAKAI_ID, nullable = true)
95+
private String siteTemplate;
96+
97+
@Column(name = "REALM_TEMPLATE", length = LENGTH_SAKAI_ID, nullable = true)
98+
private String realmTemplate;
99+
100+
@Lob
101+
@Column(name = "INBOUND_ROLE_MAP", nullable = true)
102+
private String inboundRoleMap;
103+
94104
@Column(name = "OIDC_AUTH", length = LENGTH_URI, nullable = true)
95105
private String oidcAuth;
96106

plus/impl/src/main/java/org/sakaiproject/plus/impl/PlusServiceImpl.java

+6-18
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,8 @@ public Launch updateAll(LaunchJWT launchJWT, Tenant tenant)
300300
membership = new Membership();
301301
membership.setSubject(subject);
302302
membership.setContext(context);
303-
if (launchJWT.isInstructor() ) {
304-
membership.setRole(Membership.ROLE_INSTRUCTOR);
305-
} else {
306-
membership.setRole(Membership.ROLE_LEARNER);
307-
}
303+
String ltiRoles = launchJWT.getLTI11Roles();
304+
if ( StringUtils.isNotBlank(ltiRoles) ) membership.setLtiRoles(ltiRoles);
308305
membership = membershipRepository.upsert(membership);
309306
}
310307

@@ -399,14 +396,8 @@ public Map<String,String> getPayloadFromLaunchJWT(Tenant tenant, LaunchJWT launc
399396
payload.put(BasicLTIConstants.LIS_PERSON_NAME_FAMILY, launchJWT.family_name);
400397
// payload.put(BasicLTIConstants.LIS_PERSON_NAME_MIDDLE, launchJWT.middle_name);
401398

402-
if ( launchJWT.roles != null ) {
403-
StringBuilder roles = new StringBuilder();
404-
for (String role : launchJWT.roles) {
405-
if ( roles.length() > 0 ) roles.append(',');
406-
roles.append(role);
407-
}
408-
if ( roles.length() > 0 ) payload.put(BasicLTIConstants.ROLES, roles.toString());
409-
}
399+
String ltiRoles = launchJWT.getLTI11Roles();
400+
if ( StringUtils.isNotBlank(ltiRoles) ) payload.put(BasicLTIConstants.ROLES, ltiRoles);
410401

411402
// TODO: Ask for this in custom...
412403
// payload.put(BasicLTIConstants.USER_IMAGE, );
@@ -703,11 +694,8 @@ public void syncSiteMemberships(String contextGuid, Site site) throws LTIExcepti
703694
Membership membership = new Membership();
704695
membership.setSubject(subject);
705696
membership.setContext(context);
706-
if (launchJWT.isInstructor() ) {
707-
membership.setRole(Membership.ROLE_INSTRUCTOR);
708-
} else {
709-
membership.setRole(Membership.ROLE_LEARNER);
710-
}
697+
String ltiRoles = launchJWT.getLTI11Roles();
698+
if ( StringUtils.isNotBlank(ltiRoles) ) membership.setLtiRoles(ltiRoles);
711699
membership = membershipRepository.upsert(membership);
712700

713701
Map<String, String> payload = getPayloadFromLaunchJWT(tenant, launchJWT);

plus/impl/src/main/java/org/sakaiproject/plus/impl/repository/MembershipRepositoryImpl.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,16 @@ public Membership upsert(Membership entity) {
6666
if ( newEntity == null ) return save(entity);
6767

6868
boolean unchanged =
69-
Objects.equals(entity.getRole(), newEntity.getRole())
70-
&& Objects.equals(entity.getRoleOverride(), newEntity.getRoleOverride())
69+
Objects.equals(entity.getLtiRoles(), newEntity.getLtiRoles())
70+
&& Objects.equals(entity.getLtiRolesOverride(), newEntity.getLtiRolesOverride())
7171
;
7272

7373
if ( unchanged ) return newEntity;
7474

7575
// Do the UPDATE variant of UPSERT
76-
newEntity.setRole(entity.getRole());
77-
newEntity.setRoleOverride(entity.getRoleOverride());
76+
newEntity.setLtiRoles(entity.getLtiRoles());
77+
newEntity.setLtiRolesOverride(entity.getLtiRolesOverride());
78+
newEntity.setLtiRoles(entity.getLtiRoles());
7879
return save(newEntity);
7980
}
8081

0 commit comments

Comments
 (0)