diff --git a/app/lib/error/calnet_error.rb b/app/lib/error/calnet_error.rb new file mode 100644 index 00000000..9d95cff1 --- /dev/null +++ b/app/lib/error/calnet_error.rb @@ -0,0 +1,6 @@ +module Error + # Raised calnet error when it returns an unexpected response, + # e.g. missing email value because of the schema attribute name changed unexpected by Calnet from 'berkeleyEduAlternateId' to 'berkeleyEduAlternateID' . + class CalnetError < ApplicationError + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 227b170c..3bef2cf1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,6 +7,21 @@ class User FRAMEWORK_ADMIN_GROUP = 'cn=edu:berkeley:org:libr:framework:LIBR-framework-admins,ou=campus groups,dc=berkeley,dc=edu'.freeze ALMA_ADMIN_GROUP = 'cn=edu:berkeley:org:libr:framework:alma-admins,ou=campus groups,dc=berkeley,dc=edu'.freeze + CALNET_ATTRS = { + affiliations: 'berkeleyEduAffiliations', + cs_id: 'berkeleyEduCSID', + groups: 'berkeleyEduIsMemberOf', + # student_id: 'berkeleyEduStuID', + ucpath_id: 'berkeleyEduUCPathID', + email: 'berkeleyEduAlternateID', + department_number: 'departmentNumber', + display_name: 'displayName', + employee_id: 'employeeNumber', + given_name: 'givenName', + surname: 'surname', + uid: 'uid' + }.freeze + class << self # Returns a new user object from the given "omniauth.auth" hash. That's a # hash of all data returned by the auth provider (in our case, calnet). @@ -26,26 +41,50 @@ def from_omniauth(auth) # rubocop:disable Metrics/MethodLength def auth_params_from(auth) auth_extra = auth['extra'] + verify_calnet_attributes!(auth_extra) cal_groups = auth_extra['berkeleyEduIsMemberOf'] || [] # NOTE: berkeleyEduCSID should be same as berkeleyEduStuID for students { - affiliations: auth_extra['berkeleyEduAffiliations'], - cs_id: auth_extra['berkeleyEduCSID'], - department_number: auth_extra['departmentNumber'], - display_name: auth_extra['displayName'], - email: auth_extra['berkeleyEduAlternateID'], - employee_id: auth_extra['employeeNumber'], - given_name: auth_extra['givenName'], - student_id: auth_extra['berkeleyEduStuID'], - surname: auth_extra['surname'], - ucpath_id: auth_extra['berkeleyEduUCPathID'], - uid: auth_extra['uid'] || auth['uid'], + affiliations: auth_extra[CALNET_ATTRS[:affiliations]], + cs_id: auth_extra[CALNET_ATTRS[:cs_id]], + department_number: auth_extra[CALNET_ATTRS[:department_number]], + display_name: auth_extra[CALNET_ATTRS[:display_name]], + email: auth_extra[CALNET_ATTRS[:email]], + employee_id: auth_extra[CALNET_ATTRS[:employee_id]], + given_name: auth_extra[CALNET_ATTRS[:given_name]], + student_id: auth_extra[CALNET_ATTRS[:cs_id]], + surname: auth_extra[CALNET_ATTRS[:surname]], + ucpath_id: auth_extra[CALNET_ATTRS[:ucpath_id]], + uid: auth_extra[CALNET_ATTRS[:uid]] || auth['uid'], framework_admin: cal_groups.include?(FRAMEWORK_ADMIN_GROUP), alma_admin: cal_groups.include?(ALMA_ADMIN_GROUP) } end # rubocop:enable Metrics/MethodLength + + # Verifies that auth_extra contains all required CalNet attributes with exact case-sensitive names + # Raise [Error::CalnetError] if any required attributes are missing + def verify_calnet_attributes!(auth_extra) + required_attributes = CALNET_ATTRS.values + + missing = required_attributes.reject { |attr| auth_extra.key?(attr) } + + return if missing.empty? + + current_calnet_keys = list_auth_extra_keys(auth_extra) + msg = "Cannot find CalNet schema attribute(s) (case-sensitive): #{missing.join(', ')}. The current CalNet schema attributes: #{current_calnet_keys.join(', ')}." + Rails.logger.error(msg) + raise Error::CalnetError, msg + end + + # list all keys except duo keys + def list_auth_extra_keys(auth_extra) + keys = auth_extra.keys.reject { |k| k.start_with?('duo') }.sort + Rails.logger.info("CalNet auth_extra keys: #{keys.join(', ')}") + keys + end + end # Affiliations per CalNet (attribute `berkeleyEduAffiliations` e.g. diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 058c343d..44bd026b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -21,6 +21,28 @@ expect { User.from_omniauth(auth) }.to raise_error(Error::InvalidAuthProviderError) end + it 'rejects calnet when a required schema attribute is missing or renamed' do + auth = { + 'provider' => 'calnet', + 'extra' => { + 'berkeleyEduAffiliations' => 'expected affiliation', + 'berkeleyEduCSID' => 'expected cs id', + 'berkeleyEduIsMemberOf' => [], + 'berkeleyEduStuID' => 'expected student id', + 'berkeleyEduUCPathID' => 'expected UC Path ID', + 'berkeleyEduAlternatid' => 'expected email', # intentionally wrong case to simulate renamed attribute: berkeleyEduAlternatid instead of berkeleyEduAlternateId + 'departmentNumber' => 'expected dept. number', + 'displayName' => 'expected display name', + 'employeeNumber' => 'expected employee ID', + 'givenName' => 'expected given name', + 'surname' => 'expected surname', + 'uid' => 'expected UID' + } + } + + expect { User.from_omniauth(auth) }.to raise_error(Error::CalnetError, /Missing required CalNet attributes/) + end + it 'populates a User object' do framework_admin_ldap = 'cn=edu:berkeley:org:libr:framework:LIBR-framework-admins,ou=campus groups,dc=berkeley,dc=edu' auth = {