-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow admin to create/signup users with password #151
Comments
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! 🤗 |
They aren't. That's not a use case we (currently) address. And I'm not sure we should. I'm not sure what your experiences are with signup spam or attacks(?) but maybe they could also be mitigated by having only a short signup period, in coordination with your users? |
Thanks for the response! I raised how users could be added without the signup page open as I wasn’t sure why that possibility was exposed through the config. Regarding limited signup periods, I’m using Kubernetes/Z2JH, so I’m not sure how practical that would be. As far as I know, the hub pod would need to be restarted, which would clear the state (?), or the Nonetheless, my system requires having a dynamic set of users that changes by the week. The users’ usage periods are relatively transitory, (weeks at most, mostly a single week). Admins having complete access to their work and passwords is very normal in the context … where the main concern is making it as easy for the user as possible while keeping unwanted actors out. This is probably where I’d disagree with your response … Opening up a signup page to the internet and not allowing the creation of user accounts through a closed system … to ensure admins don’t know users’ passwords feels like a strange balance to me. In my experience, users are interested in the resource and understand that privacy from us admins isn’t guaranteed (our work is also all on a shared EFS drive) I’ve had a security issue before with my cluster, so not opening up a signup page is preferable. Though, however easy it is to spam, I don’t see it being a security issue … unless it could become a DOS attack … Given these needs, direct admin control over the user credentials, at least for the initial password, is really the obvious approach for us. Currently we achieve this with LDAP, but it’s relatively cumbersome for our needs. The simplicity and cleanness of the native Authenticator is really nice and streamlines the setup of a Kubernetes jupyter hub too. I think it would be nice for it to have this flexibility, which would make it the obvious choice for a number of setups IMO. Perhaps the change password option could be made an obvious link in the login page to ensure people are aware of the option? Thanks for your response again … and of course your work on Jupyter!! |
This issue has been mentioned on Jupyter Community Forum. There might be relevant details there: https://discourse.jupyter.org/t/ldap-not-working-ldapstarttlserror/11047/3 |
It would not be very difficult to implement, but after thinking about it, I'm still not sure if we want this feature. |
If we are looking for a way for admins to have users pre-authorized, I think we should instead of pre-creating and setting passwords instead allow for the creation of one-time signup tokens that can be passed as query parameters as part of an url. Another option would be to rely on email verification and allow users to signup if they are listed in a set of allowed emails, but then that requires the email verification setup to be enabled etc which requires having a SMTP service configured. I'm not sure how to go about it, but setting username/password combinations ahead of time is a strategy I think would be a problematic pattern because it would be harder to implement, and it would also be less secure as the admin would need to manage the user's password temporarily and the user would maybe keep using it instead of changing it, etc. By instead making a single check if the user is allowed to sign up is a cleaner way to not involve this feature's logic in all steps, making it easier to implement and maintain. |
Thank you, I see we share the same concern about introducing a feature that defaults to low security. But the general use-case is still interesting here, I think. Maybe the simplest way to do it would be to add a configuration option like @meeseeksmachine, would that still work for your usecase? You'd proabably have to restart the hub to use the new configuration every time you want to add or remove an entry from the allow-list. |
@lambdaTotoro presuming by First, thanks for the engagement and discussion here! Restarting JupyerHub on Kubernetes clustersAs for restarting the hub pod/container on a kubernetes setup ... it sounds viable (off the cuff) but not ideal. I'd even wonder about whether that's a pattern of admin behaviour that Jupyter (in general) would want to rely on and advocate for. Sure, anyone administering a cluster would (and should) be able to do this. But I'm not sure it's in Jupyter's best interests to offer an authentication workflow that is dependant on being relatively comfortable with kubernetes admin, both for the purposes of offering a good user experience, and for developing a coherent ecosystem of tools. General ImplementationTo me, both your suggestion of a A potentially simple way of implementing this with one-time tokens or passwords could involve requiring users to reset their password when the appropriate config options are set ... I can imagine such an implementation being possible while using a good amount of the existing machinery. Otherwise, I do like your idea of a simple If setting these users was exposed to the admin (through an API or http form), I would imagine that there wouldn't be any need for any restart. The admin could, if necessary, distribute the one-time tokens as needed. Unsure if there are fatal security issues around that. My quick hackIf it's at all valuable, I've add below my first attempt at extending the NativeAuthenticator to allow for the behaviour I'm after here. This code was only a proof of concept, I may circle back onto implementing this later in the year. My current authentication workflowJust to clarify where I'm coming from here, I thought I'd clarify the authentication workflow I'm dealing with.
So far we've been relying on LDAP for this. But this is problematic as I've mentioned above, not least because Jupyter support for LDAP is (understandably) not super robust. Extending NativeAuthenticator to allow admin definition of usersThis code can be used as a custom config for jupyterhub or in the extraConfig options in the yams file for kubernetes. Two APIs (intended to be used purely as web APIs) are added here. One that lets admin users sign up when
The other allows admin users to add users (incl their passwords) to the user database ( from nativeauthenticator.nativeauthenticator import (
NativeAuthenticator, bcrypt)
from nativeauthenticator.handlers import (
LocalBase, admin_only, UserInfo, web)
class AdminUserSignUpHandler(LocalBase):
"""admin API for adding users"""
# this will be deprecated in jupyterhub 2.X,
# which will have more flexible roles
@admin_only
async def post(self):
username = self.get_body_argument('username', strip=False)
# could add "is_authorized": True ... no need to authorize
user_info = {
'username': username,
'pw': self.get_body_argument('pw', strip=False),
'is_authorized': True # this is trusting API, authorize straight away
}
taken = self.authenticator.user_exists(user_info['username'])
# custom create user function `admin_create_user`
user = self.authenticator.admin_create_user(**user_info)
message = self.authenticator.create_message(taken, username, user)
self.finish(message)
class AdminSignUpHandler(AdminUserSignUpHandler):
'''Allow signup for admin when no signup allowed
'''
async def post(self):
username = self.get_body_argument('username', strip=False)
# override default behaviour only under these conditions
username_is_admin = username in self.admin_users
taken = self.authenticator.user_exists(username)
special_case = (
# else, just use normal interface
(not self.authenticator.enable_signup) and
(username_is_admin) and
# only allow admin signup once!
(not taken)
)
self.log.info(f'Admin signup ... special case: {special_case}')
self.log.info(f'(is admin: {username_is_admin}, taken: {taken})')
if special_case:
await super().post()
user_info = {
'username': username,
'pw': self.get_body_argument('pw', strip=False),
'is_authorized': True
}
# custom create user function `admin_create_user`
user = self.authenticator.admin_create_user(**user_info)
message = self.authenticator.create_message(taken, username, user)
self.finish(message)
else:
raise web.HTTPError(404)
class AdminNativeAuthenticator(NativeAuthenticator):
def get_handlers(self, app):
# hope this super call works!
handlers = super().get_handlers(app)
handlers.append(
(r'/admin-signup', AdminSignUpHandler))
handlers.append(
(r'/admin-user-signup', AdminUserSignUpHandler))
return handlers
def create_message(self, taken, username, user):
"Create dictionary message for use in lightweight admin API"
if taken:
message = {
'message': "Username {} is taken".format(username),
'status': 'taken'
}
elif user:
message = {
# presuming user is a UserInfo object as all other are None
'message': 'Username {} has been added'.format(user.username),
'status': 'success'
}
else:
message = {
'message': "Error, username {} not added".format(username),
'status': 'error'
}
return message
def admin_create_user(self, username, pw, **kwargs):
"""Simple direct user creation for trustworthy/admin caller
"""
# at this stage ... just lowercase
# NativeAuthenticator and base Authenticator just lower ... could add more
username = self.normalize_username(username)
encoded_pw = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
infos = {'username': username, 'password': encoded_pw}
infos.update(kwargs)
try:
user_info = UserInfo(**infos)
except AssertionError:
return
self.db.add(user_info)
self.db.commit()
return user_info
# perhaps needs to be updated
c.JupyterHub.authenticator_class = AdminNativeAuthenticator
# c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'
# no signup ... how admin login?
c.Authenticator.enable_signup = False
# c.Authenticator.enable_signup = True
c.Authenticator.admin_users = {'errol'} |
That I did. Sorry for tagging the wrong m-account. ^^ I've given this some more thought and I think I like @consideRatio's idea of one-time signup tokens that any admin can generate at will the most. The way it's all taking shape in my head is as follows (input warmly welcomed): If signup is generally not allowed, an admin could navigate to a page (could be part of the authorization area or somewhere else) and generate one-time signup tokens that can be distributed via whatever channel you use to communicate with your userbase. The users could then navigate to The machinery for these signatures/token already exists, so the implementation effort would not be very large. It would also ensure that users always set passwords themselves and the hub doesn't need to be restarted every time. We would only have to think about what to do about multiple signups using the same token. Maybe limit them to a shot lifespan? I will have to think more on that. This is a change I'd be happy to have in nativeAuthenticator, without reservations about security, and I think it would also cover your use case. |
No worries on the tagging! Both meaningless m words! Your suggestion sounds good to me! Just curious though, what’s the pre-existing token machinery you have in mind? I might have missed it in my perusal of the code base, and if I’m to hack together a temporary solution for myself, using it could be a better way to go. |
Whoops, finger slipped. By pre-existing machinery, I was referring to @davidedelvento's work on #145. The cryptographic tokens for signup wouldn't be very different from the ones for self-serve authorization, I assume. |
I think that #145 and #146 already provide almost all that is needed. How about reading a file of allowed users instead of using a regular expression as in #145 ? Assuming the file can be actually open/read/closed and never cached, that will also solve the restart server issue. The #146 should mitigate the possible high load certainly from DoS attacks? In my opinion the big problem with account creation is not the admins knowing the password, but communicating them to users. Of course that is because I assume people are not in the same room (who is these days with COVID?) otherwise it's a moot point. Alternatively, a simple solution (which require a missing piece) could be the following:
@maegul would that work for your use case? I think having the "forgot password" page is something that will be useful to more people (and hence easier to get accepted as a PR and maintained) than just an ad-hoc solution for your case only which may eventually become stale... Just my $0.02 |
@davidedelvento I’m totally with you on ensuring this results in a generally useful solution rather than something useful for me. My attempt to generalise the feature so far has been to express it as “admin controlled predefinition of user accounts, with no open signup and preferably a flexible interface (where http forms are probably flexible enough)”. I would imagine others would find this useful, including those not yet using nativeauthenticator (including myself). Unless I’ve missed something, you’re suggestions imply that user signup would/should remain open. I wouldn’t agree with this …
Otherwise, I agree, extending regex user definition to an allow list user definition is the essence of this feature request. I agree about the passwords, and my use case might be special in how we treat passwords, though I personally don’t think I quite understand the magnitude of the concerns expressed by the maintainers here and lean more on the side of giving admins flexibility than trying to ensure no bad practice can occur by limiting functionality. For instance, because we generate the passwords, we force all of them to be strong, which many users would readily avoid or find cumbersome if left to manage their passwords on their own. But that’s my $0.02 🙃 As for the forgot password page, while I understand your urge to generalise a solution, my immediate response is this is conflating two things rather than generalising. For instance, I would rather not allow a forgot passwords functionality and require some reset to go through the admin on request. In the end, I probably differ with you on how ad-hoc my feature request is. I think there are two broad categories of operation here. Open, longer term, probably university/academia based operations. And, more closed, commercial, dynamic, more confidentiality sensitive with regard to the whole cluster/server but not necessarily between users, kind of operations. The former has driven the interfaces of the jupyter-native authenticators, AFAICT, while other outfits/operations have been expected to go with the other options (??). Native authenticator seems to be hitting a sweet spot for anything medium-scale downward, with enough flexibility to cover many use cases, IMO, and I think a closed user creation interface fits naturally into that. If there’s genuine concern over how niche/ad-hoc this feature would be, perhaps laying out the magnitude of the work required and sampling the desirability of it (from users of the authenticators maybe) might make sense? |
I think you are going too much into the weeds here, but instead of replying philosophically to your general comments, let me be specific and practical. Since you said:
Go ahead and implement it, I'm sure the owners/administrators (I'm not one) will accept a pull request for that, and you can explicitly ask if it's important to know in advance. I developed the original code for that section and I think it's something easy to add. Just to be clear: I am somebody like you who saw the potential of nativeauthenticator to solve our problems, but it was missing things we needed. As such I wrote the code for those functionalities and I contributed it back with 3 pull requests. I don't think the project owners/maintainers have any spare to implement the feature request for you, so you'll have to do it yourself (or hire a student to do it for you). |
You underestimate how many other tasks I'm willing to procrastinate for this project. I did a little experimenting on the Here's how it looks currently. If signup is disabled, the authorization area gets a new section in which you can copy signup-URLs that are valid for three days and than can be used to sign up to the system (as many times as one likes). @maegul |
😂 … I guess I’m personally glad to hear it!! @lambdaTotoro this looks great! It would definitely help me out and hopefully others too. When/if I get a chance to try it out I’ll give more detailed feedback. Off the top of my head, some points I can see others, incl maybe myself, raising:
Probably thoughts for another day though if at all! Thanks! 🙏 |
This is easily implemented with another config variable that just defaults to 3 days but can be set to any number of minutes(?) instead.
This is a little more peculiar, I'd probably push that to a later release because I'd need to think about it more. But if this is generally helpful, I'll tidy up the branch and do the documentation properly. Then, we can do a PR. |
Hello, I want to expose my situation because I think it is usual, and I don't think the solution found can solve this case. I understand the security issues but I would really like to be able to create the users and associate them with a jupyterhub group before the training starts. Possible workflow: Thanks |
Glad to hear your input here @djangoliv. We're probably in similar positions, ie training that isn't done through an academic semester long subject (which doesn't necessarily exclude academic contexts, FWIW). I would imagine it's not an uncommon application of the Jupyter ecosystem, and one which furthers Jupyter IMO as, in my context at least, it serves as an effective way of introducing people to Jupyter. Otherwise, though haven't familiarised myself with JupyterHub 2 yet and the new roles/groups system, it seems that NativeAuthenticator hasn't either ??!!. Which means that the way it integrates with the problems you raise is still an open question. Moreover, does NativeAuthenticator even work on JupyterHub2 yet?? |
@maegul : Yes I use NativeAuthenticator with jupyter2 (with a pip install git+https://github.com/jupyterhub/nativeauthenticator.git |
Obligatory annoying nag ... how's this tracking @lambdaTotoro ? Any rough sense of a timeline here? Anything we can do to help at all? |
@maegul, I use a script to automatically register (signup) all my students. import requests
signup_url = <your jupyterhub>/hub/signup
password = <common password for all users>
trainees = ["user1", "user2", "user3"]
for login in trainees:
resp = requests.post(
signup_url,
data={
"username": login,
"signup_password": password,
"signup_password_confirmation": password,
},
)
if resp.ok:
print(f"Account created for {login} as login")
else:
print(
f"Account creation failed for {login} as login"
) |
@djangoliv Thanks! I thought of doing something similar ... but then I was asked to close the open endpoint. I've settled on a relatively easy solution which involves passing in a custom script under Happy to share it wherever convenient. Thanks again though!! |
Could you please post this script? It is actually what I need: create users and close the signup endpoint. Even it would be great to automatically authorized those ones. |
Here's the code (where the code below is put simply into the YAML config under extraConfig:
my_config.py: |
import json
from nativeauthenticator.nativeauthenticator import (
NativeAuthenticator, bcrypt)
from nativeauthenticator.handlers import (
LocalBase, admin_only, UserInfo, web)
class AdminUserSignUpHandler(LocalBase):
"""admin API for adding users"""
# this will be deprecated in jupyterhub 2.X,
# which will have more flexible roles
@admin_only
async def post(self):
# expecting form encoded data (ie, use data in requests.post())
username = self.get_body_argument('username', strip=False)
pw = self.get_body_argument('pw', strip=False)
user_info = {
'username': username,
'pw': pw,
'is_authorized': True # this is trusting API, authorize straight away
}
taken = self.authenticator.user_exists(user_info['username'])
# custom create user function `admin_create_user`
user = self.authenticator.admin_create_user(**user_info)
message = self.authenticator.create_message(taken, username, user)
self.finish(message)
class AdminSignUpHandler(AdminUserSignUpHandler):
'''Allow signup for admin when no signup allowed
'''
async def post(self):
username = self.get_body_argument('username', strip=False)
# override default behaviour only under these conditions
username_is_admin = username in self.admin_users
taken = self.authenticator.user_exists(username)
special_case = (
# else, just use normal interface
(not self.authenticator.enable_signup) and
(username_is_admin) and
# only allow admin signup once!
(not taken)
)
self.log.info(f'Admin_users: {self.admin_users}')
self.log.info(f'Admin signup ... special case: {special_case}')
self.log.info(f'(is admin: {username_is_admin}, taken: {taken})')
if special_case:
# await super().post()
user_info = {
'username': username,
'pw': self.get_body_argument('pw', strip=False),
'is_authorized': True
}
# custom create user function `admin_create_user`
user = self.authenticator.admin_create_user(**user_info)
message = self.authenticator.create_message(taken, username, user)
self.finish(message)
else:
raise web.HTTPError(404)
class AdminNativeAuthenticator(NativeAuthenticator):
def get_handlers(self, app):
# hope this super call works!
handlers = super().get_handlers(app)
handlers.append(
(r'/admin-signup', AdminSignUpHandler))
handlers.append(
(r'/admin-user-signup', AdminUserSignUpHandler))
return handlers
def create_message(self, taken, username, user):
"Create dictionary message for use in lightweight admin API"
if taken:
message = {
'message': "Username {} is taken".format(username),
'status': 'taken'
}
elif user:
message = {
# presuming user is a UserInfo object as all other are None
'message': 'Username {} has been added'.format(user.username),
'status': 'success'
}
else:
message = {
'message': "Error, username {} not added".format(username),
'status': 'error'
}
return message
def admin_create_user(self, username, pw, **kwargs):
"""Simple direct user creation for trustworthy/admin caller
"""
# at this stage ... just lowercase
# NativeAuthenticator and base Authenticator just lower ... could add more
username = self.normalize_username(username)
encoded_pw = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
infos = {'username': username, 'password': encoded_pw}
infos.update(kwargs)
try:
user_info = UserInfo(**infos)
except AssertionError:
return
self.db.add(user_info)
self.db.commit()
return user_info
# use custom authenticator
c.JupyterHub.authenticator_class = AdminNativeAuthenticator
# no signup ... all logins, including initial admin signup, must be through API
c.Authenticator.enable_signup = False |
Proposed change
/authorize
page for instance) to signup new usersusers_info
table, and the user can go straight to the/login
page, and login with the password the admin created, without having to signup.Alternative options
/signup
option and page open is vulnerable to attack/spamWho would use this feature?
I note that this has been previously raised in #23, but without any discussion at all, so sorry for the duplication, but I thought this a decent enough idea that it was worth raising again
(Optional): Suggest a solution
/authorize
pagePOST
tohub_url/signup
), oradmin_signup
endpoint that uses the underlyingself.authenticator.create_user()
(which should hopefully be simple as the admin's choices should be trust worthy) ... and hit this endpoint with the form instead.The text was updated successfully, but these errors were encountered: