Skip to content
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

Open
maegul opened this issue Oct 5, 2021 · 25 comments
Open

Allow admin to create/signup users with password #151

maegul opened this issue Oct 5, 2021 · 25 comments
Labels
enhancement New feature or request

Comments

@maegul
Copy link

maegul commented Oct 5, 2021

Proposed change

  • Allow the admin (in the /authorize page for instance) to signup new users
  • This action should be the equivalent of when a new user would signup
    • ie, a new entry is created in the users_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

  • Having the /signup option and page open is vulnerable to attack/spam
  • Closing the signup option removes the ability to dynamically add or remove users as necessary.
    • infact, I'm confused how users are to be created at all when the signup page is closed

Who would use this feature?

  • Anyone running a jupyterhub server that wants minimal hassle for their users, who simply receive an email with a URL and a password for instance, but wants to retain admin control over credentials,
  • who wants an easy to maintain authentication setup
  • without the vulnerability to attack/spam
  • that is potentially easy to automate the creation or removal of new users

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

  • Add a signup form to the /authorize page
  • have the form hit the same endpoint as the signup page (ie, POST to hub_url/signup), or
  • because that might create some interference with some of the other config options (eg captcha), make a simple admin_signup endpoint that uses the underlying self.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.
  • This could be always available or dependent on config.
@maegul maegul added the enhancement New feature or request label Oct 5, 2021
@welcome
Copy link

welcome bot commented Oct 5, 2021

Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! 🤗

If you haven't done so already, check out Jupyter's Code of Conduct. Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
welcome
You can meet the other Jovyans by joining our Discourse forum. There is also an intro thread there where you can stop by and say Hi! 👋

Welcome to the Jupyter community! 🎉

@lambdaTotoro
Copy link
Collaborator

infact, I'm confused how users are to be created at all when the signup page is closed

They aren't. That's not a use case we (currently) address. And I'm not sure we should.
Generally, it's preferable for users to sign up themselves. This avoids having to send them the password and also the admin knowing the password (it can be changed after, but how many users actually do?). That's gotta be weighed against any added convenience.

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?

@maegul
Copy link
Author

maegul commented Oct 5, 2021

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 jupyterhub process on the hub pod would need to be restarted, which I’m unsure about.

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!!

@meeseeksmachine
Copy link

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

@lambdaTotoro
Copy link
Collaborator

It would not be very difficult to implement, but after thinking about it, I'm still not sure if we want this feature.
@consideRatio, do you have any thoughts?

@consideRatio
Copy link
Member

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.

@lambdaTotoro
Copy link
Collaborator

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 limited_signup (as opposed to open_signup, those would probably be mutually exclusive) that keeps an allow-list of usernames or emails (or both 🤔).
So if anyone from the allow-list tries to sign up it goes through and everyone else gets an error. Since we have reCAPTCHA support now, that could also protect against scripting attacks trying to brute-force their way in.

@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.

@maegul
Copy link
Author

maegul commented Nov 4, 2021

@lambdaTotoro presuming by @meeseeksmachine you meant me (meeseeksmachine is a bog AFAICT) ...

First, thanks for the engagement and discussion here!

Restarting JupyerHub on Kubernetes clusters

As 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 Implementation

To me, both your suggestion of a limited_signup parameter and @consideRatio 's of a one-time signup token are great and would be great way to implement this basic behaviour of a pre-defined and closed set of users defined ahead of time by an admin. Perhaps ideally your limited_signup could be employed with one-time tokens as an optional additional layer of security.

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 allow_list.

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 hack

If 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 workflow

Just to clarify where I'm coming from here, I thought I'd clarify the authentication workflow I'm dealing with.

  • Kubernetes JupyterHub (Z2JH)
  • Dynamic user base: adding and removing users every week
  • Users are under time constraints, such that a simple login process is highly desirable
  • We run an automated user setup process:
    • that takes user email addresses as input
    • and sends emails to new users with usernames (generated from the email addresses) and randomly generated passwords (as well as the necessary link for logging in)

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 users

This code can be used as a custom config for jupyterhub or in the extraConfig options in the yams file for kubernetes.
My quick test with jupyterhub shows this to be functional at least.

Two APIs (intended to be used purely as web APIs) are added here.

One that lets admin users sign up when enable_signup is false. (/admin-signup)

  • Unless I'm missing something, this is a curious deadens available in the config ... what is the point of enable_signup? ... presumably for when user credentials are being inherited from a previous database?

The other allows admin users to add users (incl their passwords) to the user database (/admin-user-signup).

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'}

@lambdaTotoro
Copy link
Collaborator

@maegul; presuming by @/meeseeksmachine you meant me

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 /hub/signup/<token> (which would just redirect to /hub/signup when anyone can sign up) that lets them sign up the usual way.

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.

@maegul
Copy link
Author

maegul commented Nov 5, 2021

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.

@lambdaTotoro
Copy link
Collaborator

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.

@davidedelvento
Copy link

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:

  • the admin creates the users one-by-one with the regular account creation page, and the user receive the email link to activate the account as with Allow some users (but not all) to not need admin approval #145 (this might be too tedious depending on the number of users)
  • the account creation page could made available to admins only so others don't see it (either via configuration parameter, or simply by a code modification before deploying: IIRC it's just a decorator)
  • the passwords used by the admins are arbitrary and never communicated to the user
  • the users use the "forgot password" page (which is the missing piece: AFAIK there isn't any) to set their password

@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

@maegul
Copy link
Author

maegul commented Nov 6, 2021

@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 …

  1. Allowing something like Allow some users (but not all) to not need admin approval #145 to work with closed signup and the allow-list of users you and @lambdaTotoro are suggesting wouldn’t, AFAICT, be inconsistent with the current code base. Or at least not dramatically.
  2. Providing some closed signup functionality allows this authenticator to provide for use cases where the LDAPAuthenticator (or others) would be used instead (thus my interest), but more lightweight and flexible, which seems to be the mission/purpose of nativeauthenticator.

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?

@davidedelvento
Copy link

@maegul

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:

Otherwise, I agree, extending regex user definition to an allow list user definition is the essence of this feature request.

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).

@lambdaTotoro
Copy link
Collaborator

I don't think the project owners/maintainers have any spare to implement the feature request for you

You underestimate how many other tasks I'm willing to procrastinate for this project. I did a little experimenting on the admin-signup-token branch to see how this would work out.

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).

Screenshot 2021-11-13 at 20-59-56 JupyterHub

@maegul
From what I have in mind, this should cover your use case, right? Do you have any feedback or things you'd like changed?

@maegul
Copy link
Author

maegul commented Nov 16, 2021

You underestimate how many other tasks I'm willing to procrastinate for this project.

😂 … 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:

  • Allow custom token validity period (other than 3 days)
  • Expose API for generating / getting the URL + token (where automatic Workflows would be presumably easier)

Probably thoughts for another day though if at all!

Thanks! 🙏

@lambdaTotoro
Copy link
Collaborator

Allow custom token validity period (other than 3 days)

This is easily implemented with another config variable that just defaults to 3 days but can be set to any number of minutes(?) instead.

Expose API for generating / getting the URL + token [...]

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.

@djangoliv
Copy link
Contributor

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 use jupyterhub for training. Often, I have between 10 and 20 students for 1 or 2 days of training.
So at the start of the training they all have to register, then I have to authorize them and they log in.
With jupyterhub2 I also have to put them in a particular group (and they have to be connected before they appear in jupyterhub).
All of this is time consuming and laborious.

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:
Would it be possible to be able to create a batch of users and link them to a group without specifying a password?
Then users would be directly connected to the sign'up (by sending them their identifier, or in a personal link with a token that points to their account), given that they would already be authorized and assigned to a group.

Thanks

@maegul
Copy link
Author

maegul commented Jan 19, 2022

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??

@djangoliv
Copy link
Contributor

@maegul : Yes I use NativeAuthenticator with jupyter2 (with a pip install git+https://github.com/jupyterhub/nativeauthenticator.git
). The roles/groups are starting to be considered, but there are still things to do (including on the jupyterhub side ).

@maegul
Copy link
Author

maegul commented Mar 15, 2022

Obligatory annoying nag ... how's this tracking @lambdaTotoro ? Any rough sense of a timeline here? Anything we can do to help at all?

@djangoliv
Copy link
Contributor

@maegul, I use a script to automatically register (signup) all my students.
After, i just have to manually authorize them.

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"
        )

@maegul
Copy link
Author

maegul commented May 24, 2022

@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 extraConfig. It involves subclassing NativeAuthenticator to add a couple of custom endpoints that mostly leverage the existing API.

Happy to share it wherever convenient.

Thanks again though!!

@jlanza
Copy link

jlanza commented Oct 4, 2022

@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 extraConfig. It involves subclassing NativeAuthenticator to add a couple of custom endpoints that mostly leverage the existing API.

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.

@maegul
Copy link
Author

maegul commented Oct 5, 2022

Here's the code (where the code below is put simply into the YAML config under extraConfig.
My comments in the code on my uncertainties should indicate how reliable this is 😄

  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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants