Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/lib/error/calnet_error.rb
Original file line number Diff line number Diff line change
@@ -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
61 changes: 50 additions & 11 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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(', ')}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this value is already being logged above (line 76), does it need to be logged here as well?

keys
end

end

# Affiliations per CalNet (attribute `berkeleyEduAffiliations` e.g.
Expand Down
22 changes: 22 additions & 0 deletions spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading