Authentication on Hue using SAML SSO and Azure Part 2
This post is going to go over an issue that took a huge amount of debugging to understand and uncover while I was implementing SAML Authentication with HUE. After reviewing limited documentation, failed trial and error configuration attempts and ultimately an ignored StackOverflow question (Spoiler here if interested) it became evident I was going to have to figure this one out myself.
To see other adventures in HUE SAML configuration, see my other post here
The Problem Statement
After setting up HUE to work with SAML authentication, it was reported to me that the users expecting to have administrator access to the UI were not getting it. This was surprising to me as for virtually every instance I set up, I would log in afterward, where I was set up as an administrator and I had administrator access. After confirming that the users weren’t doing anything incorrect I set out to find out what was going on.
Background Information
I mentioned this in my first post in this series that in my implementation, I prepopulate all the users in the HUE database, which is done by using the the HUE shell with the following commands:
build/env/bin/hue shell
This launches a python-based, interactive shell that we can run commands in. To create a user, the following commands one after another will do the job:
from django.contrib.auth.models import User
user = User.objects.create(username='dave')
user.set_password('davehillrocks')
user.save()
If setting a user up with admin access, there is only one additional line to add before the last line where the user is saved which is:
user.is_superuser = True
We can check any user’s status by using the shell like this:
from django.contrib.auth.models import User
user = User.objects.get(username='dave')
print user.is_superuser
TRUE
I was able to use these steps to validate that I was setting up the users correctly, admins were being setup with is_superuser = True
and this was visible before they logged in BUT after they logged in, they were losing it. So here’s what I did next.
Investigation
The first thing I wanted to understand was why I was seemingly unaffected by this issue, when all the other admins were. I checked to see on the Azure SSO Application side if there was a difference in how our users were being retrieved for validation but there was nothing there. The first breakthrough came when I went over every instance of HUE I had set up and discovered there was a single instance where I WASN’T an admin, but another user was. After reviewing the logins by checking the auth_user
table in the hue dbshell
, I discovered the other user who was an admin has logged in before me which suggested that I wasn’t special (gasp!), but instead it was whoever logged in first that was special.
Trying to understand where this deviation came from started the investigation moving towards the SAML requests and responses to see if there was anything suspicious in them to suggest differences in behaviour. After comparing logins of the first and subsequent users, there were two things that only appeared in the first login attempts:
backend INFO Augmenting users with class: <class 'desktop.auth.backend.DefaultUserAugmentor'>
and
mdstore ERROR Unsupported binding: urn:oasis:names:tc:SAML:2.0:bindings:SOAP (https://sts.windows.net/xxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxxxx/)
mdstore ERROR Unsupported binding: urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST (https://sts.windows.net/xxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxxxx/)
The Solution
At this point, it had become pretty clear that the issue was no longer one controlled by configuration, and there was a need to start digging into the code. To begin this investation, I looked up the desktop.auth.backend.DefaultUserAugmentor
class. While this class further suggested that behaviour on the first login would differ from subsequent logins, it still didn’t address this issue. Reviewing the code and the various imports, the path led towards the backend
files, which each abstract a different authorisation mechanism. A number of the backends are tied to specific Django libraries, but for an additional layer of complexity, the SAML backend is imported through libsaml
, based on python’s implementation of SAML, pysaml
.
Looking at the libsaml backend.py
file, finally I was able to get some answers.
71 def update_user(self, user, attributes, attribute_mapping, force_save=False):
72 # Do this check up here, because the auth call creates a django user upon first login per user
73 is_super = False
74 if not UserProfile.objects.filter(creation_method=UserProfile.CreationMethod.EXTERNAL.name).exists():
75 # If there are no LDAP users already in the system, the first one will
76 # become a superuser
77 is_super = True
The code snippet above is one referenced by the DefaultUserAugmentor
class (eventually) and the comments on lines 75 and 76 were the first real clue of what was happening. Here was code that was acting in the way I was seeing, with the first login getting superuser access alone. After testing with a normal user on one of my instances and observing that even a user not intended to be an admin was being promoted to an administrator upon an initial login, I was certain I was getting close.
78 else:
79 user = self._get_user_by_username(user.username)
80 if user is not None:
81 # If the user already exists, we shouldn't change its superuser
82 # privileges. However, if there's a naming conflict with a non-external
83 # user, we should do the safe thing and turn off superuser privs.
84 existing_profile = get_profile(user)
85 if existing_profile.creation_method == UserProfile.CreationMethod.EXTERNAL.name:
86 is_super = user.is_superuser
The rest of this method was pretty solidly indicating that the code on line 86 was not being executed. To be sure, I dropped a few subtle log messages in the code, restarted HUE and let another user login:
The code was being executed, but line 86, where the user would inherit the is_superuser
property wasn’t. As the debug messages above showed, the two values in the if
statement preceding this were not matching. existing_profile.creation_method
was resolving to HUE
and UserProfile.CreationMethod.EXTERNAL.name
equals EXTERNAL
. Essentially, the user was created in HUE and yet, because of a security check in the libsaml
library to see if the user can be compared to a user in an external system such as LDAP, and if not, it doesn’t trust the user to handle superuser powers. The use case I had presented with of prepopulating the Django backend via the HUE shell was something that was simply incompatible with the security checks baked into the library.
The solution?
84 existing_profile = get_profile(user)
85 #if existing_profile.creation_method == UserProfile.CreationMethod.EXTERNAL.name:
86 is_super = user.is_superuser
With this if statement removed (and the indent modified), the users were now able to log in and retain their permissions. Three keystrokes solved a problem that took days of investigation to uncover. As is the trend in development.
I know it’s not a common use case but if this helps anyone, I’ll count that as a win!