Skip to content

Commit 1672eaa

Browse files
authored
[System:Feature] SAML mapping for registration feed (#24)
* Autofeed Update As requested by @bmcutler * Duplicate User ID enrollments no longer invalidate course data. First enrollment is accepted and other enrollments are discarded. * Blank email addresses are accepted as empty strings. * Update ssaf_sql.php User emails cannot be updated with a blank email. * SAML Mappings Provide insert to SAML mappings for students.
1 parent 7e268ae commit 1672eaa

File tree

5 files changed

+77
-16
lines changed

5 files changed

+77
-16
lines changed

student_auto_feed/config.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@
105105
//Header row, if it exists, must be discarded during processing.
106106
define('HEADER_ROW_EXISTS', true);
107107

108+
//Set to true, if Submitty is using SAML for authentication.
109+
define('PROCESS_SAML', true);
110+
108111
//Allows "\r" EOL encoding. This is rare but exists (e.g. Excel for Macintosh).
109112
ini_set('auto_detect_line_endings', true);
110113

student_auto_feed/ssaf_db.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ public static function upsert($semester, $course, $rows) : bool {
159159
case self::run_query(sql::LOCK_COURSES_USERS, null) === false:
160160
self::$error .= "\nError during LOCK courses_users table, {$course}";
161161
return false;
162+
case self::run_query(sql::LOCK_SAML_MAPPED_USERS, null) === false:
163+
self::$error .= "\nError during LOCK saml_mapped_users table, {$course}";
164+
return false;
162165
}
163166

164167
// Do upsert of course enrollment data.
@@ -207,13 +210,22 @@ public static function upsert($semester, $course, $rows) : bool {
207210
}
208211
} // END row by row processing.
209212

210-
// Finish up by checking for dropped students.
213+
// Process students who dropped a course.
211214
if (self::run_query(sql::DROPPED_USERS, $dropped_users_params) === false) {
212215
self::run_query(sql::ROLLBACK, null);
213216
self::$error .= "\nError processing dropped students, {$course}\n";
214217
return false;
215218
}
216219

220+
// Add students to SAML mappings when PROCESS_SAML is set to true in config.php.
221+
if (PROCESS_SAML) {
222+
if (self::run_query(sql::INSERT_SAML_MAP, null) === false) {
223+
self::run_query(sql::ROLLBACK, null);
224+
self::$error .= "\nError processing saml mappings, {$course}\n";
225+
return false;
226+
}
227+
}
228+
217229
// All data has been upserted. Complete transaction and return success or failure.
218230
return self::run_query(sql::COMMIT, null) !== false;
219231
}

student_auto_feed/ssaf_sql.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class sql {
77
public const LOCK_COURSES = "LOCK TABLE courses IN EXCLUSIVE MODE";
88
public const LOCK_REG_SECTIONS = "LOCK TABLE courses_registration_sections IN EXCLUSIVE MODE";
99
public const LOCK_COURSES_USERS = "LOCK TABLE courses_users IN EXCLUSIVE MODE";
10+
public const LOCK_SAML_MAPPED_USERS = "LOCK TABLE saml_mapped_users IN EXCLUSIVE MODE";
1011
public const BEGIN = "BEGIN";
1112
public const COMMIT = "COMMIT";
1213
public const ROLLBACK = "ROLLBACK";
@@ -55,7 +56,11 @@ class sql {
5556
THEN EXCLUDED.user_preferred_firstname
5657
ELSE users.user_preferred_firstname
5758
END,
58-
user_email=EXCLUDED.user_email
59+
user_email=
60+
CASE WHEN COALESCE(EXCLUDED.user_email, '')<>''
61+
THEN EXCLUDED.user_email
62+
ELSE users.user_email
63+
END
5964
/* AUTH: "AUTO_FEED" */
6065
SQL;
6166

@@ -127,6 +132,19 @@ class sql {
127132
AND courses_users.user_group=4
128133
AND courses_users.manual_registration=FALSE
129134
SQL;
135+
136+
public const INSERT_SAML_MAP = <<<SQL
137+
INSERT INTO saml_mapped_users (
138+
saml_id,
139+
user_id
140+
) SELECT tmp.user_id, tmp.user_id
141+
FROM tmp_enrolled tmp
142+
LEFT OUTER JOIN saml_mapped_users saml1 ON tmp.user_id = saml1.user_id
143+
LEFT OUTER JOIN saml_mapped_users saml2 ON tmp.user_id = saml2.saml_id
144+
WHERE saml1.user_id IS NULL
145+
AND saml2.saml_id IS NULL
146+
SQL;
147+
130148
}
131149

132150
//EOF

student_auto_feed/ssaf_validate.php

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
Q: What is going on with the regex for email validation in
44
validate::validate_row()?
55
A: The regex is intending to match an email address as
6+
1. An empty string. Obviously not a real email address, but we are allowing
7+
enrollments in the database with "inactive" or "pending" email addresses.
8+
-- OR --
69
1. Address recipient may not contain characters (),:;<>@\"[]
710
2. Address recipient may not start or end with characters !#$%'*+-/=?^_`{|
811
3. Address recipient and hostname must be delimited with @ character
@@ -13,6 +16,7 @@
1316
8. The entire email address is case insensitive
1417
1518
Peter Bailie, Oct 29 2021
19+
Last Updated May 12, 2022 by Peter Bailie
1620
----------------------------------------------------------------------------- */
1721

1822
namespace ssaf;
@@ -48,24 +52,24 @@ public static function validate_row($row, $row_num) : bool {
4852
case is_null(EXPECTED_TERM_CODE) ? true : $row[COLUMN_TERM_CODE] === EXPECTED_TERM_CODE:
4953
self::$error = "Row {$row_num} failed validation for unexpected term code \"{$row[COLUMN_TERM_CODE]}\".";
5054
return false;
51-
// User ID must contain only lowercase alpha, numbers, underscore, and hyphen
52-
case boolval(preg_match("/^[a-z0-9_\-]+$/", $row[COLUMN_USER_ID])):
55+
// User ID must contain only alpha characters, numbers, underscore, hyphen, and period.
56+
case boolval(preg_match("/^[a-z0-9_\-\.]+$/i", $row[COLUMN_USER_ID])):
5357
self::$error = "Row {$row_num} failed user ID validation \"{$row[COLUMN_USER_ID]}\".";
5458
return false;
5559
// First name must be alpha characters, white-space, or certain punctuation.
56-
case boolval(preg_match("/^[a-zA-Z'`\-\. ]+$/", $row[COLUMN_FIRSTNAME])):
60+
case boolval(preg_match("/^[a-z'`\-\. ]+$/i", $row[COLUMN_FIRSTNAME])):
5761
self::$error = "Row {$row_num} failed validation for student first name \"{$row[COLUMN_FIRSTNAME]}\".";
5862
return false;
5963
// Last name must be alpha characters, white-space, or certain punctuation.
60-
case boolval(preg_match("/^[a-zA-Z'`\-\. ]+$/", $row[COLUMN_LASTNAME])):
64+
case boolval(preg_match("/^[a-z'`\-\. ]+$/i", $row[COLUMN_LASTNAME])):
6165
self::$error = "Row {$row_num} failed validation for student last name \"{$row[COLUMN_LASTNAME]}\".";
6266
return false;
6367
// Student registration section must be alphanumeric, '_', or '-'.
64-
case boolval(preg_match("/^[a-zA-Z0-9_\-]+$/", $row[COLUMN_SECTION])):
68+
case boolval(preg_match("/^[a-z0-9_\-]+$/i", $row[COLUMN_SECTION])):
6569
self::$error = "Row {$row_num} failed validation for student section \"{$row[COLUMN_SECTION]}\".";
6670
return false;
67-
// Check email address is properly formed.
68-
case boolval(preg_match("/^(?![!#$%'*+\-\/=?^_`{|])[^(),:;<>@\\\"\[\]]+(?<![!#$%'*+\-\/=?^_`{|])@(?:(?!\-)[a-z0-9\-]+(?<!\-)\.)+[a-z]{2,}$/i", $row[COLUMN_EMAIL])):
71+
// Check email address is properly formed. Blank email addresses are also accepted.
72+
case boolval(preg_match("/^$|^(?![!#$%'*+\-\/=?^_`{|])[^(),:;<>@\\\"\[\]]+(?<![!#$%'*+\-\/=?^_`{|])@(?:(?!\-)[a-z0-9\-]+(?<!\-)\.)+[a-z]{2,}$/i", $row[COLUMN_EMAIL])):
6973
self::$error = "Row {$row_num} failed validation for student email \"{$row[COLUMN_EMAIL]}\".";
7074
return false;
7175
}
@@ -85,24 +89,34 @@ public static function validate_row($row, $row_num) : bool {
8589
* False, as in error found, otherwise. $user_ids is filled when return
8690
* is FALSE.
8791
*
88-
* @param array $rows Data rows to check (presumably an entire couse)
89-
* @param string[] &$user_id Duplicated user ID, when found
92+
* @param array $rows Data rows to check (presumably an entire couse).
93+
* @param string[] &$user_id Duplicated user ID, when found.
94+
* @param string[] &$d_rows Rows containing duplicate user IDs, indexed by user ID.
9095
* @return bool TRUE when all user IDs are unique, FALSE otherwise.
9196
*/
92-
public static function check_for_duplicate_user_ids(array $rows, &$user_ids) : bool {
97+
public static function check_for_duplicate_user_ids(array $rows, &$user_ids, &$d_rows) : bool {
9398
usort($rows, function($a, $b) { return $a[COLUMN_USER_ID] <=> $b[COLUMN_USER_ID]; });
9499

95100
$user_ids = array();
101+
$d_rows = array();
96102
$are_all_unique = true; // Unless proven FALSE
97103
$length = count($rows);
98104
for ($i = 1; $i < $length; $i++) {
99105
$j = $i - 1;
100106
if ($rows[$i][COLUMN_USER_ID] === $rows[$j][COLUMN_USER_ID]) {
101107
$are_all_unique = false;
102-
$user_ids[] = $rows[$i][COLUMN_USER_ID];
108+
$user_id = $rows[$i][COLUMN_USER_ID];
109+
$user_ids[] = $user_id;
110+
$d_rows[$user_id][] = $j;
111+
$d_rows[$user_id][] = $i;
103112
}
104113
}
105114

115+
foreach($d_rows as &$d_row) {
116+
array_unique($d_row, SORT_REGULAR);
117+
}
118+
unset($d_row);
119+
106120
return $are_all_unique;
107121
}
108122

student_auto_feed/submitty_student_auto_feed.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ private function get_csv_data() {
175175
if (array_search($course, $this->course_list) !== false) {
176176
if (validate::validate_row($row, $row_num)) {
177177
$this->data[$course][] = $row;
178+
// Rows with blank emails are allowed, but they are being logged.
179+
if ($row[COLUMN_EMAIL] === "") {
180+
$this->log_it("Blank email found for user {$row[COLUMN_USER_ID]}, row {$row_num}.");
181+
}
178182
} else {
179183
$this->invalid_courses[$course] = true;
180184
$this->log_it(validate::$error);
@@ -228,9 +232,19 @@ private function get_csv_data() {
228232
private function check_for_duplicate_user_ids() {
229233
foreach($this->data as $course => $rows) {
230234
$user_ids = null;
231-
// returns FALSE (as in there is an error) when duplicate IDs are found.
232-
if (validate::check_for_duplicate_user_ids($rows, $user_ids) === false) {
233-
$this->invalid_courses[$course] = true;
235+
$d_rows = null;
236+
// Returns FALSE (as in there is an error) when duplicate IDs are found.
237+
// However, a duplicate ID does not invalidate a course. Instead, the
238+
// first enrollment is accepted, the other enrollments are discarded,
239+
// and the event is logged.
240+
if (validate::check_for_duplicate_user_ids($rows, $user_ids, $d_rows) === false) {
241+
foreach($d_rows as $user_id => $userid_rows) {
242+
$length = count($userid_rows);
243+
for ($i = 1; $i < $length; $i++) {
244+
unset($this->data[$course][$userid_rows[$i]]);
245+
}
246+
}
247+
234248
$msg = "Duplicate user IDs detected in {$course} data: ";
235249
$msg .= implode(", ", $user_ids);
236250
$this->log_it($msg);

0 commit comments

Comments
 (0)