Skip to content

Commit e4ffe4b

Browse files
committed
Use the downcased freeipa principal name, otherwise the provided username
Because sssd supports logins with email or userids by default[1], we need to ensure we're using the same user for the same backend external auth account, such as freeipa. In this case, we need to fetch the krbPrincipalName user attribute or if coming from httpd request headers, the X-REMOTE-USER-PRINCIPAL, which under the covers also comes from the krbPrincipalName value. This krbPrincipalName is the backend account name with the kerberos domain and is consistent if you login with: first.last@example.com (email) flast@IPA.test(ipa userid) flast@ipa.test(case insensitive) flast(ipa userid without the domain) Previously, the email login would create a user with username of: first.last. Existing username normalization occurs in the authenticate method. Since we're overriding this behavior in the subclass for systems that have the krbPrincipalName user attribute or from httpd header with key X-REMOTE-USER-PRINCIPAL, we need to ensure these values are also normalized or we could have an existing user created with a downcased userid and a new user with uppercase characters in the userid. [1] Logins with either the userid or the email address is a feature of SSSD, the daemon for central access to the various identity providers in linux. You can see they updated the documentation to better describe this feature in: https://issues.redhat.com/browse/RHEL-1654 They released this doc in: https://access.redhat.com/errata/RHBA-2024:9351 There's a little more found here: https://www.github.com/SSSD/sssd/issues/7136 There is some configuration that can be done to disable this, see the last link. "If for some reason several users need to share the same email address then set this option to a nonexistent attribute name in order to disable user lookup/login by email."
1 parent 0446227 commit e4ffe4b

File tree

2 files changed

+79
-22
lines changed

2 files changed

+79
-22
lines changed

app/models/authenticator/httpd.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ def find_userid_as_distinguished_name(user_attrs)
123123
User.in_my_region.where("userid LIKE ?", "%=#{user_attrs[:username]},%,#{dn_domain}").last
124124
end
125125

126+
# TODO: If we now have the principal name via krbPrincipalName, we can use that instead of this method.
127+
# In order to remove this method, we'd need to make sure all identity providers using httpd expose the principal name in the same format, even if it's just the username.
126128
def username_to_upn_name(user_attrs)
127129
return user_attrs[:username] if user_attrs[:domain].nil?
128130

@@ -133,7 +135,8 @@ def username_to_upn_name(user_attrs)
133135

134136
def user_details_from_external_directory(username)
135137
ext_user_attrs = user_attrs_from_external_directory(username)
136-
user_attrs = {:username => username,
138+
principal_username = ext_user_attrs["krbPrincipalName"] && normalize_username(ext_user_attrs["krbPrincipalName"])
139+
user_attrs = {:username => principal_username || username,
137140
:fullname => ext_user_attrs["displayname"],
138141
:firstname => ext_user_attrs["givenname"],
139142
:lastname => ext_user_attrs["sn"],
@@ -153,7 +156,8 @@ def user_details_from_headers(username, request)
153156

154157
delimiter = self.class.group_delimiter || user_headers['X-REMOTE-USER-GROUP-DELIMITER'].presence || /[;:,]/
155158
groups = CGI.unescape(user_headers['X-REMOTE-USER-GROUPS'] || '').split(delimiter)
156-
user_attrs = {:username => username,
159+
principal_username = user_headers['X-REMOTE-USER-PRINCIPAL'] && normalize_username(user_headers['X-REMOTE-USER-PRINCIPAL'])
160+
user_attrs = {:username => principal_username || username,
157161
:fullname => user_headers['X-REMOTE-USER-FULLNAME'],
158162
:firstname => user_headers['X-REMOTE-USER-FIRSTNAME'],
159163
:lastname => user_headers['X-REMOTE-USER-LASTNAME'],
@@ -164,6 +168,8 @@ def user_details_from_headers(username, request)
164168
end
165169

166170
private def user_headers_from_request(request)
171+
# NOTE: X-REMOTE-USER-PRINCIPAL must be set in the headers by the sssd/apache configuration
172+
# with a value of the krbPrincipalName or some other reasonable value based on the identity provider.
167173
%w[
168174
X-REMOTE-USER
169175
X-REMOTE-USER-FULLNAME
@@ -173,6 +179,7 @@ def user_details_from_headers(username, request)
173179
X-REMOTE-USER-DOMAIN
174180
X-REMOTE-USER-GROUPS
175181
X-REMOTE-USER-GROUP-DELIMITER
182+
X-REMOTE-USER-PRINCIPAL
176183
].each_with_object({}) do |k, h|
177184
h[k] = request.headers[k]&.force_encoding("UTF-8")
178185
end.delete_nils
@@ -198,7 +205,7 @@ def user_attrs_from_external_directory(username)
198205
end
199206
end
200207

201-
ATTRS_NEEDED = %w[mail givenname sn displayname domainname].freeze
208+
ATTRS_NEEDED = %w[mail givenname sn displayname domainname krbPrincipalName].freeze
202209

203210
def user_attrs_from_external_directory_via_dbus(username)
204211
return unless username

spec/models/authenticator/httpd_spec.rb

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,19 @@
9393
{
9494
'X-Remote-User' => 'cheshire',
9595
'X-Remote-User-FullName' => 'Cheshire Cat',
96-
'X-Remote-User-FirstName' => 'Chechire',
96+
'X-Remote-User-FirstName' => 'Cheshire',
9797
'X-Remote-User-LastName' => 'Cat',
9898
'X-Remote-User-Email' => 'cheshire@example.com',
9999
'X-Remote-User-Domain' => 'example.com',
100100
'X-Remote-User-Groups' => 'wibble@fqdn:bubble@fqdn',
101101
}
102102
end
103103

104+
it "finds the user based on the downcased X-Remote-User-Principal header" do
105+
headers['X-Remote-User-Principal'] = 'cheshire@EXAMPLE.COM'
106+
expect(subject.lookup_by_identity('baduser', request)).to eq(cheshire)
107+
end
108+
104109
it "Handles missing request parameter" do
105110
expect(subject.lookup_by_identity('alice')).to eq(alice)
106111
end
@@ -644,7 +649,7 @@ def authenticate
644649
end
645650
end
646651

647-
describe ".user_attrs_from_external_directory_via_dbus" do
652+
describe "with dbus mocks" do
648653
let(:ifp_interface) { double('ifp_interface') }
649654
before do
650655
require "dbus"
@@ -659,28 +664,73 @@ def authenticate
659664
allow(ifp_object).to receive(:[]).with("org.freedesktop.sssd.infopipe").and_return(ifp_interface)
660665
end
661666

662-
it "should return nil for unspecified user" do
663-
expect(subject.send(:user_attrs_from_external_directory_via_dbus, nil)).to be_nil
664-
end
667+
describe ".user_attrs_from_external_directory_via_dbus" do
668+
it "should return nil for unspecified user" do
669+
expect(subject.send(:user_attrs_from_external_directory_via_dbus, nil)).to be_nil
670+
end
671+
672+
it "should return user attributes hash for valid user" do
673+
requested_attrs = %w[mail givenname sn displayname domainname krbPrincipalName]
665674

666-
it "should return user attributes hash for valid user" do
667-
requested_attrs = %w[mail givenname sn displayname domainname]
675+
jdoe_attrs = [{"mail" => ["jdoe@example.com"],
676+
"givenname" => ["John"],
677+
"sn" => ["Doe"],
678+
"displayname" => ["John Doe"],
679+
"domainname" => ["ipa.test"],
680+
"krbPrincipalName" => ["jdoe@IPA.TEST"]}]
681+
682+
expected_jdoe_attrs = {"mail" => "jdoe@example.com",
683+
"givenname" => "John",
684+
"sn" => "Doe",
685+
"displayname" => "John Doe",
686+
"domainname" => "ipa.test",
687+
"krbPrincipalName" => "jdoe@IPA.TEST"}
688+
689+
allow(ifp_interface).to receive(:GetUserAttr).with('jdoe', requested_attrs).and_return(jdoe_attrs)
690+
691+
result = subject.send(:user_attrs_from_external_directory_via_dbus, 'jdoe')
692+
expect(result).to eq(expected_jdoe_attrs)
693+
end
668694

669-
jdoe_attrs = [{"mail" => ["jdoe@example.com"],
670-
"givenname" => ["John"],
671-
"sn" => ["Doe"],
672-
"displayname" => ["John Doe"],
673-
"domainname" => ["example.com"]}]
695+
it "should handle missing krbPrincipalName gracefully" do
696+
requested_attrs = %w[mail givenname sn displayname domainname krbPrincipalName]
674697

675-
expected_jdoe_attrs = {"mail" => "jdoe@example.com",
676-
"givenname" => "John",
677-
"sn" => "Doe",
678-
"displayname" => "John Doe",
679-
"domainname" => "example.com"}
698+
jdoe_attrs = [{"mail" => ["jdoe@example.com"],
699+
"givenname" => ["John"],
700+
"sn" => ["Doe"],
701+
"displayname" => ["John Doe"],
702+
"domainname" => ["ipa.test"]}]
680703

681-
allow(ifp_interface).to receive(:GetUserAttr).with('jdoe', requested_attrs).and_return(jdoe_attrs)
704+
expected_jdoe_attrs = {"mail" => "jdoe@example.com",
705+
"givenname" => "John",
706+
"sn" => "Doe",
707+
"displayname" => "John Doe",
708+
"domainname" => "ipa.test",
709+
"krbPrincipalName" => nil}
682710

683-
expect(subject.send(:user_attrs_from_external_directory_via_dbus, 'jdoe')).to eq(expected_jdoe_attrs)
711+
allow(ifp_interface).to receive(:GetUserAttr).with('jdoe', requested_attrs).and_return(jdoe_attrs)
712+
713+
expect(subject.send(:user_attrs_from_external_directory_via_dbus, 'jdoe')).to eq(expected_jdoe_attrs)
714+
end
715+
end
716+
717+
describe "#user_details_from_external_directory" do
718+
it "should normalize krbPrincipalName when present" do
719+
requested_attrs = %w[mail givenname sn displayname domainname krbPrincipalName]
720+
721+
jdoe_attrs = [{"mail" => ["jdoe@example.com"],
722+
"givenname" => ["John"],
723+
"sn" => ["Doe"],
724+
"displayname" => ["John Doe"],
725+
"domainname" => ["ipa.test"],
726+
"krbPrincipalName" => ["jdoe@IPA.TEST"]}]
727+
728+
allow(ifp_interface).to receive(:GetUserAttr).with('jdoe', requested_attrs).and_return(jdoe_attrs)
729+
allow(MiqGroup).to receive(:get_httpd_groups_by_user).with('jdoe').and_return([])
730+
731+
user_attrs, _groups = subject.send(:user_details_from_external_directory, 'jdoe')
732+
expect(user_attrs[:username]).to eq("jdoe@ipa.test")
733+
end
684734
end
685735
end
686736
end

0 commit comments

Comments
 (0)