We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Default shell to bash
Right now, if users are created by jupyterhub with useradd on a stock Ubuntu, there is no default shell. This leads to `/bin/sh` being used rather than bash. When the web terminal is opened all readline related actions (tab, up, down) don't work. I know I can configure this in `/etc/default/useradd`, but we jut added an option to set the home directory base path. Should we default the shell to bash in the useradd CLI call? Maybe add an config option that defaults to `/bin/bash`. This behavior means that a pretty standard config on Ubuntu will have a (slightly) broken terminal.

I could have sworn that we did something on this previously, when I was debugging things with my Active Directory setup where `SHELL` wasn't being set. But I don't remember what we ended up doing (I may have just set it myself).

Thoughts?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `jupyterhub/auth.py`
Content:
```
1 """Simple PAM authenticator"""
2 
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5 
6 from grp import getgrnam
7 import pwd
8 from subprocess import check_call, check_output, CalledProcessError
9 
10 from tornado import gen
11 import pamela
12 
13 from traitlets.config import LoggingConfigurable
14 from traitlets import Bool, Set, Unicode, Any
15 
16 from .handlers.login import LoginHandler
17 from .utils import url_path_join
18 
19 class Authenticator(LoggingConfigurable):
20     """A class for authentication.
21     
22     The API is one method, `authenticate`, a tornado gen.coroutine.
23     """
24     
25     db = Any()
26     admin_users = Set(config=True,
27         help="""set of usernames of admin users
28 
29         If unspecified, only the user that launches the server will be admin.
30         """
31     )
32     whitelist = Set(config=True,
33         help="""Username whitelist.
34         
35         Use this to restrict which users can login.
36         If empty, allow any user to attempt login.
37         """
38     )
39     custom_html = Unicode('',
40         help="""HTML login form for custom handlers.
41         Override in form-based custom authenticators
42         that don't use username+password,
43         or need custom branding.
44         """
45     )
46     login_service = Unicode('',
47         help="""Name of the login service for external
48         login services (e.g. 'GitHub').
49         """
50     )
51     
52     @gen.coroutine
53     def authenticate(self, handler, data):
54         """Authenticate a user with login form data.
55         
56         This must be a tornado gen.coroutine.
57         It must return the username on successful authentication,
58         and return None on failed authentication.
59         """
60 
61     def pre_spawn_start(self, user, spawner):
62         """Hook called before spawning a user's server.
63         
64         Can be used to do auth-related startup, e.g. opening PAM sessions.
65         """
66     
67     def post_spawn_stop(self, user, spawner):
68         """Hook called after stopping a user container.
69         
70         Can be used to do auth-related cleanup, e.g. closing PAM sessions.
71         """
72     
73     def check_whitelist(self, user):
74         """
75         Return True if the whitelist is empty or user is in the whitelist.
76         """
77         # Parens aren't necessary here, but they make this easier to parse.
78         return (not self.whitelist) or (user in self.whitelist)
79 
80     def add_user(self, user):
81         """Add a new user
82         
83         By default, this just adds the user to the whitelist.
84         
85         Subclasses may do more extensive things,
86         such as adding actual unix users.
87         """
88         if self.whitelist:
89             self.whitelist.add(user.name)
90     
91     def delete_user(self, user):
92         """Triggered when a user is deleted.
93         
94         Removes the user from the whitelist.
95         """
96         self.whitelist.discard(user.name)
97     
98     def login_url(self, base_url):
99         """Override to register a custom login handler"""
100         return url_path_join(base_url, 'login')
101     
102     def logout_url(self, base_url):
103         """Override to register a custom logout handler"""
104         return url_path_join(base_url, 'logout')
105     
106     def get_handlers(self, app):
107         """Return any custom handlers the authenticator needs to register
108         
109         (e.g. for OAuth)
110         """
111         return [
112             ('/login', LoginHandler),
113         ]
114 
115 class LocalAuthenticator(Authenticator):
116     """Base class for Authenticators that work with local *ix users
117     
118     Checks for local users, and can attempt to create them if they exist.
119     """
120     
121     create_system_users = Bool(False, config=True,
122         help="""If a user is added that doesn't exist on the system,
123         should I try to create the system user?
124         """
125     )
126     system_user_home = Unicode('', config=True,
127         help="""Specify root home directory for users if different from system default.
128         
129         Passed to `useradd -b`.
130         If specified, when system users are created their home directories will be created in
131         
132             system_user_home/username
133         
134         Only has an effect when users are created with `create_system_users=True`.
135         """
136     )
137 
138     group_whitelist = Set(
139         config=True,
140         help="Automatically whitelist anyone in this group.",
141     )
142 
143     def _group_whitelist_changed(self, name, old, new):
144         if self.whitelist:
145             self.log.warn(
146                 "Ignoring username whitelist because group whitelist supplied!"
147             )
148 
149     def check_whitelist(self, username):
150         if self.group_whitelist:
151             return self.check_group_whitelist(username)
152         else:
153             return super().check_whitelist(username)
154 
155     def check_group_whitelist(self, username):
156         if not self.group_whitelist:
157             return False
158         for grnam in self.group_whitelist:
159             try:
160                 group = getgrnam(grnam)
161             except KeyError:
162                 self.log.error('No such group: [%s]' % grnam)
163                 continue
164             if username in group.gr_mem:
165                 return True
166         return False
167 
168     @gen.coroutine
169     def add_user(self, user):
170         """Add a new user
171         
172         By default, this just adds the user to the whitelist.
173         
174         Subclasses may do more extensive things,
175         such as adding actual unix users.
176         """
177         user_exists = yield gen.maybe_future(self.system_user_exists(user))
178         if not user_exists:
179             if self.create_system_users:
180                 yield gen.maybe_future(self.add_system_user(user))
181             else:
182                 raise KeyError("User %s does not exist." % user.name)
183         
184         yield gen.maybe_future(super().add_user(user))
185     
186     @staticmethod
187     def system_user_exists(user):
188         """Check if the user exists on the system"""
189         try:
190             pwd.getpwnam(user.name)
191         except KeyError:
192             return False
193         else:
194             return True
195     
196     def add_system_user(self, user):
197         """Create a new *ix user on the system. Works on FreeBSD and Linux, at least."""
198         name = user.name
199         for useradd in (
200             ['pw', 'useradd', '-m'],
201             ['useradd', '-m'],
202         ):
203             try:
204                 check_output(['which', useradd[0]])
205             except CalledProcessError:
206                 continue
207             else:
208                 break
209         else:
210             raise RuntimeError("I don't know how to add users on this system.")
211         
212         cmd = useradd
213         if self.system_user_home:
214             cmd = cmd + ['-b', self.system_user_home]
215         check_call(cmd + [name])
216 
217 
218 class PAMAuthenticator(LocalAuthenticator):
219     """Authenticate local *ix users with PAM"""
220     encoding = Unicode('utf8', config=True,
221         help="""The encoding to use for PAM"""
222     )
223     service = Unicode('login', config=True,
224         help="""The PAM service to use for authentication."""
225     )
226     
227     @gen.coroutine
228     def authenticate(self, handler, data):
229         """Authenticate with PAM, and return the username if login is successful.
230     
231         Return None otherwise.
232         """
233         username = data['username']
234         if not self.check_whitelist(username):
235             return
236         try:
237             pamela.authenticate(username, data['password'], service=self.service)
238         except pamela.PAMError as e:
239             if handler is not None:
240                 self.log.warn("PAM Authentication failed (@%s): %s", handler.request.remote_ip, e)
241             else:
242                 self.log.warn("PAM Authentication failed: %s", e)
243         else:
244             return username
245     
246     def pre_spawn_start(self, user, spawner):
247         """Open PAM session for user"""
248         try:
249             pamela.open_session(user.name, service=self.service)
250         except pamela.PAMError as e:
251             self.log.warn("Failed to open PAM session for %s: %s", user.name, e)
252     
253     def post_spawn_stop(self, user, spawner):
254         """Close PAM session for user"""
255         try:
256             pamela.close_session(user.name, service=self.service)
257         except pamela.PAMError as e:
258             self.log.warn("Failed to close PAM session for %s: %s", user.name, e)
259     
260
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py
--- a/jupyterhub/auth.py
+++ b/jupyterhub/auth.py
@@ -4,8 +4,11 @@
 # Distributed under the terms of the Modified BSD License.
 
 from grp import getgrnam
+import pipes
 import pwd
-from subprocess import check_call, check_output, CalledProcessError
+from shutil import which
+import sys
+from subprocess import check_call
 
 from tornado import gen
 import pamela
@@ -15,6 +18,7 @@
 
 from .handlers.login import LoginHandler
 from .utils import url_path_join
+from .traitlets import Command
 
 class Authenticator(LoggingConfigurable):
     """A class for authentication.
@@ -123,17 +127,36 @@
         should I try to create the system user?
         """
     )
-    system_user_home = Unicode('', config=True,
-        help="""Specify root home directory for users if different from system default.
+    add_user_cmd = Command(config=True,
+        help="""The command to use for creating users as a list of strings.
         
-        Passed to `useradd -b`.
-        If specified, when system users are created their home directories will be created in
+        For each element in the list, the string USERNAME will be replaced with
+        the user's username. The username will also be appended as the final argument.
         
-            system_user_home/username
+        For Linux, the default value is:
         
-        Only has an effect when users are created with `create_system_users=True`.
+            ['adduser', '-q', '--gecos', '""', '--disabled-password']
+            
+        To specify a custom home directory, set this to:
+        
+            ['adduser', '-q', '--gecos', '""', '--home', '/customhome/USERNAME', '--disabled-password']
+
+        This will run the command:
+
+        adduser -q --gecos "" --home /customhome/river --disabled-password river
+        
+        when the user 'river' is created.
         """
     )
+    def _add_user_cmd_default(self):
+        if sys.platform == 'darwin':
+            raise ValueError("I don't know how to create users on OS X")
+        elif which('pw'):
+            # Probably BSD
+            return ['pw', 'useradd', '-m']
+        else:
+            # This appears to be the Linux non-interactive adduser command:
+            return ['adduser', '-q', '--gecos', '""', '--disabled-password']
 
     group_whitelist = Set(
         config=True,
@@ -196,23 +219,9 @@
     def add_system_user(self, user):
         """Create a new *ix user on the system. Works on FreeBSD and Linux, at least."""
         name = user.name
-        for useradd in (
-            ['pw', 'useradd', '-m'],
-            ['useradd', '-m'],
-        ):
-            try:
-                check_output(['which', useradd[0]])
-            except CalledProcessError:
-                continue
-            else:
-                break
-        else:
-            raise RuntimeError("I don't know how to add users on this system.")
-        
-        cmd = useradd
-        if self.system_user_home:
-            cmd = cmd + ['-b', self.system_user_home]
-        check_call(cmd + [name])
+        cmd = [ arg.replace('USERNAME', name) for arg in self.add_user_cmd ] + [name]
+        self.log.info("Creating user: %s", ' '.join(map(pipes.quote, cmd)))
+        check_call(cmd)
 
 
 class PAMAuthenticator(LocalAuthenticator):

{"golden_diff": "diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py\n--- a/jupyterhub/auth.py\n+++ b/jupyterhub/auth.py\n@@ -4,8 +4,11 @@\n # Distributed under the terms of the Modified BSD License.\n \n from grp import getgrnam\n+import pipes\n import pwd\n-from subprocess import check_call, check_output, CalledProcessError\n+from shutil import which\n+import sys\n+from subprocess import check_call\n \n from tornado import gen\n import pamela\n@@ -15,6 +18,7 @@\n \n from .handlers.login import LoginHandler\n from .utils import url_path_join\n+from .traitlets import Command\n \n class Authenticator(LoggingConfigurable):\n     \"\"\"A class for authentication.\n@@ -123,17 +127,36 @@\n         should I try to create the system user?\n         \"\"\"\n     )\n-    system_user_home = Unicode('', config=True,\n-        help=\"\"\"Specify root home directory for users if different from system default.\n+    add_user_cmd = Command(config=True,\n+        help=\"\"\"The command to use for creating users as a list of strings.\n         \n-        Passed to `useradd -b`.\n-        If specified, when system users are created their home directories will be created in\n+        For each element in the list, the string USERNAME will be replaced with\n+        the user's username. The username will also be appended as the final argument.\n         \n-            system_user_home/username\n+        For Linux, the default value is:\n         \n-        Only has an effect when users are created with `create_system_users=True`.\n+            ['adduser', '-q', '--gecos', '\"\"', '--disabled-password']\n+            \n+        To specify a custom home directory, set this to:\n+        \n+            ['adduser', '-q', '--gecos', '\"\"', '--home', '/customhome/USERNAME', '--disabled-password']\n+\n+        This will run the command:\n+\n+        adduser -q --gecos \"\" --home /customhome/river --disabled-password river\n+        \n+        when the user 'river' is created.\n         \"\"\"\n     )\n+    def _add_user_cmd_default(self):\n+        if sys.platform == 'darwin':\n+            raise ValueError(\"I don't know how to create users on OS X\")\n+        elif which('pw'):\n+            # Probably BSD\n+            return ['pw', 'useradd', '-m']\n+        else:\n+            # This appears to be the Linux non-interactive adduser command:\n+            return ['adduser', '-q', '--gecos', '\"\"', '--disabled-password']\n \n     group_whitelist = Set(\n         config=True,\n@@ -196,23 +219,9 @@\n     def add_system_user(self, user):\n         \"\"\"Create a new *ix user on the system. Works on FreeBSD and Linux, at least.\"\"\"\n         name = user.name\n-        for useradd in (\n-            ['pw', 'useradd', '-m'],\n-            ['useradd', '-m'],\n-        ):\n-            try:\n-                check_output(['which', useradd[0]])\n-            except CalledProcessError:\n-                continue\n-            else:\n-                break\n-        else:\n-            raise RuntimeError(\"I don't know how to add users on this system.\")\n-        \n-        cmd = useradd\n-        if self.system_user_home:\n-            cmd = cmd + ['-b', self.system_user_home]\n-        check_call(cmd + [name])\n+        cmd = [ arg.replace('USERNAME', name) for arg in self.add_user_cmd ] + [name]\n+        self.log.info(\"Creating user: %s\", ' '.join(map(pipes.quote, cmd)))\n+        check_call(cmd)\n \n \n class PAMAuthenticator(LocalAuthenticator):\n", "issue": "Default shell to bash\nRight now, if users are created by jupyterhub with useradd on a stock Ubuntu, there is no default shell. This leads to `/bin/sh` being used rather than bash. When the web terminal is opened all readline related actions (tab, up, down) don't work. I know I can configure this in `/etc/default/useradd`, but we jut added an option to set the home directory base path. Should we default the shell to bash in the useradd CLI call? Maybe add an config option that defaults to `/bin/bash`. This behavior means that a pretty standard config on Ubuntu will have a (slightly) broken terminal.\n\nI could have sworn that we did something on this previously, when I was debugging things with my Active Directory setup where `SHELL` wasn't being set. But I don't remember what we ended up doing (I may have just set it myself).\n\nThoughts?\n\n", "before_files": [{"content": "\"\"\"Simple PAM authenticator\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom grp import getgrnam\nimport pwd\nfrom subprocess import check_call, check_output, CalledProcessError\n\nfrom tornado import gen\nimport pamela\n\nfrom traitlets.config import LoggingConfigurable\nfrom traitlets import Bool, Set, Unicode, Any\n\nfrom .handlers.login import LoginHandler\nfrom .utils import url_path_join\n\nclass Authenticator(LoggingConfigurable):\n    \"\"\"A class for authentication.\n    \n    The API is one method, `authenticate`, a tornado gen.coroutine.\n    \"\"\"\n    \n    db = Any()\n    admin_users = Set(config=True,\n        help=\"\"\"set of usernames of admin users\n\n        If unspecified, only the user that launches the server will be admin.\n        \"\"\"\n    )\n    whitelist = Set(config=True,\n        help=\"\"\"Username whitelist.\n        \n        Use this to restrict which users can login.\n        If empty, allow any user to attempt login.\n        \"\"\"\n    )\n    custom_html = Unicode('',\n        help=\"\"\"HTML login form for custom handlers.\n        Override in form-based custom authenticators\n        that don't use username+password,\n        or need custom branding.\n        \"\"\"\n    )\n    login_service = Unicode('',\n        help=\"\"\"Name of the login service for external\n        login services (e.g. 'GitHub').\n        \"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate a user with login form data.\n        \n        This must be a tornado gen.coroutine.\n        It must return the username on successful authentication,\n        and return None on failed authentication.\n        \"\"\"\n\n    def pre_spawn_start(self, user, spawner):\n        \"\"\"Hook called before spawning a user's server.\n        \n        Can be used to do auth-related startup, e.g. opening PAM sessions.\n        \"\"\"\n    \n    def post_spawn_stop(self, user, spawner):\n        \"\"\"Hook called after stopping a user container.\n        \n        Can be used to do auth-related cleanup, e.g. closing PAM sessions.\n        \"\"\"\n    \n    def check_whitelist(self, user):\n        \"\"\"\n        Return True if the whitelist is empty or user is in the whitelist.\n        \"\"\"\n        # Parens aren't necessary here, but they make this easier to parse.\n        return (not self.whitelist) or (user in self.whitelist)\n\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        if self.whitelist:\n            self.whitelist.add(user.name)\n    \n    def delete_user(self, user):\n        \"\"\"Triggered when a user is deleted.\n        \n        Removes the user from the whitelist.\n        \"\"\"\n        self.whitelist.discard(user.name)\n    \n    def login_url(self, base_url):\n        \"\"\"Override to register a custom login handler\"\"\"\n        return url_path_join(base_url, 'login')\n    \n    def logout_url(self, base_url):\n        \"\"\"Override to register a custom logout handler\"\"\"\n        return url_path_join(base_url, 'logout')\n    \n    def get_handlers(self, app):\n        \"\"\"Return any custom handlers the authenticator needs to register\n        \n        (e.g. for OAuth)\n        \"\"\"\n        return [\n            ('/login', LoginHandler),\n        ]\n\nclass LocalAuthenticator(Authenticator):\n    \"\"\"Base class for Authenticators that work with local *ix users\n    \n    Checks for local users, and can attempt to create them if they exist.\n    \"\"\"\n    \n    create_system_users = Bool(False, config=True,\n        help=\"\"\"If a user is added that doesn't exist on the system,\n        should I try to create the system user?\n        \"\"\"\n    )\n    system_user_home = Unicode('', config=True,\n        help=\"\"\"Specify root home directory for users if different from system default.\n        \n        Passed to `useradd -b`.\n        If specified, when system users are created their home directories will be created in\n        \n            system_user_home/username\n        \n        Only has an effect when users are created with `create_system_users=True`.\n        \"\"\"\n    )\n\n    group_whitelist = Set(\n        config=True,\n        help=\"Automatically whitelist anyone in this group.\",\n    )\n\n    def _group_whitelist_changed(self, name, old, new):\n        if self.whitelist:\n            self.log.warn(\n                \"Ignoring username whitelist because group whitelist supplied!\"\n            )\n\n    def check_whitelist(self, username):\n        if self.group_whitelist:\n            return self.check_group_whitelist(username)\n        else:\n            return super().check_whitelist(username)\n\n    def check_group_whitelist(self, username):\n        if not self.group_whitelist:\n            return False\n        for grnam in self.group_whitelist:\n            try:\n                group = getgrnam(grnam)\n            except KeyError:\n                self.log.error('No such group: [%s]' % grnam)\n                continue\n            if username in group.gr_mem:\n                return True\n        return False\n\n    @gen.coroutine\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        user_exists = yield gen.maybe_future(self.system_user_exists(user))\n        if not user_exists:\n            if self.create_system_users:\n                yield gen.maybe_future(self.add_system_user(user))\n            else:\n                raise KeyError(\"User %s does not exist.\" % user.name)\n        \n        yield gen.maybe_future(super().add_user(user))\n    \n    @staticmethod\n    def system_user_exists(user):\n        \"\"\"Check if the user exists on the system\"\"\"\n        try:\n            pwd.getpwnam(user.name)\n        except KeyError:\n            return False\n        else:\n            return True\n    \n    def add_system_user(self, user):\n        \"\"\"Create a new *ix user on the system. Works on FreeBSD and Linux, at least.\"\"\"\n        name = user.name\n        for useradd in (\n            ['pw', 'useradd', '-m'],\n            ['useradd', '-m'],\n        ):\n            try:\n                check_output(['which', useradd[0]])\n            except CalledProcessError:\n                continue\n            else:\n                break\n        else:\n            raise RuntimeError(\"I don't know how to add users on this system.\")\n        \n        cmd = useradd\n        if self.system_user_home:\n            cmd = cmd + ['-b', self.system_user_home]\n        check_call(cmd + [name])\n\n\nclass PAMAuthenticator(LocalAuthenticator):\n    \"\"\"Authenticate local *ix users with PAM\"\"\"\n    encoding = Unicode('utf8', config=True,\n        help=\"\"\"The encoding to use for PAM\"\"\"\n    )\n    service = Unicode('login', config=True,\n        help=\"\"\"The PAM service to use for authentication.\"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate with PAM, and return the username if login is successful.\n    \n        Return None otherwise.\n        \"\"\"\n        username = data['username']\n        if not self.check_whitelist(username):\n            return\n        try:\n            pamela.authenticate(username, data['password'], service=self.service)\n        except pamela.PAMError as e:\n            if handler is not None:\n                self.log.warn(\"PAM Authentication failed (@%s): %s\", handler.request.remote_ip, e)\n            else:\n                self.log.warn(\"PAM Authentication failed: %s\", e)\n        else:\n            return username\n    \n    def pre_spawn_start(self, user, spawner):\n        \"\"\"Open PAM session for user\"\"\"\n        try:\n            pamela.open_session(user.name, service=self.service)\n        except pamela.PAMError as e:\n            self.log.warn(\"Failed to open PAM session for %s: %s\", user.name, e)\n    \n    def post_spawn_stop(self, user, spawner):\n        \"\"\"Close PAM session for user\"\"\"\n        try:\n            pamela.close_session(user.name, service=self.service)\n        except pamela.PAMError as e:\n            self.log.warn(\"Failed to close PAM session for %s: %s\", user.name, e)\n    \n", "path": "jupyterhub/auth.py"}], "after_files": [{"content": "\"\"\"Simple PAM authenticator\"\"\"\n\n# Copyright (c) IPython Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom grp import getgrnam\nimport pipes\nimport pwd\nfrom shutil import which\nimport sys\nfrom subprocess import check_call\n\nfrom tornado import gen\nimport pamela\n\nfrom traitlets.config import LoggingConfigurable\nfrom traitlets import Bool, Set, Unicode, Any\n\nfrom .handlers.login import LoginHandler\nfrom .utils import url_path_join\nfrom .traitlets import Command\n\nclass Authenticator(LoggingConfigurable):\n    \"\"\"A class for authentication.\n    \n    The API is one method, `authenticate`, a tornado gen.coroutine.\n    \"\"\"\n    \n    db = Any()\n    admin_users = Set(config=True,\n        help=\"\"\"set of usernames of admin users\n\n        If unspecified, only the user that launches the server will be admin.\n        \"\"\"\n    )\n    whitelist = Set(config=True,\n        help=\"\"\"Username whitelist.\n        \n        Use this to restrict which users can login.\n        If empty, allow any user to attempt login.\n        \"\"\"\n    )\n    custom_html = Unicode('',\n        help=\"\"\"HTML login form for custom handlers.\n        Override in form-based custom authenticators\n        that don't use username+password,\n        or need custom branding.\n        \"\"\"\n    )\n    login_service = Unicode('',\n        help=\"\"\"Name of the login service for external\n        login services (e.g. 'GitHub').\n        \"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate a user with login form data.\n        \n        This must be a tornado gen.coroutine.\n        It must return the username on successful authentication,\n        and return None on failed authentication.\n        \"\"\"\n\n    def pre_spawn_start(self, user, spawner):\n        \"\"\"Hook called before spawning a user's server.\n        \n        Can be used to do auth-related startup, e.g. opening PAM sessions.\n        \"\"\"\n    \n    def post_spawn_stop(self, user, spawner):\n        \"\"\"Hook called after stopping a user container.\n        \n        Can be used to do auth-related cleanup, e.g. closing PAM sessions.\n        \"\"\"\n    \n    def check_whitelist(self, user):\n        \"\"\"\n        Return True if the whitelist is empty or user is in the whitelist.\n        \"\"\"\n        # Parens aren't necessary here, but they make this easier to parse.\n        return (not self.whitelist) or (user in self.whitelist)\n\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        if self.whitelist:\n            self.whitelist.add(user.name)\n    \n    def delete_user(self, user):\n        \"\"\"Triggered when a user is deleted.\n        \n        Removes the user from the whitelist.\n        \"\"\"\n        self.whitelist.discard(user.name)\n    \n    def login_url(self, base_url):\n        \"\"\"Override to register a custom login handler\"\"\"\n        return url_path_join(base_url, 'login')\n    \n    def logout_url(self, base_url):\n        \"\"\"Override to register a custom logout handler\"\"\"\n        return url_path_join(base_url, 'logout')\n    \n    def get_handlers(self, app):\n        \"\"\"Return any custom handlers the authenticator needs to register\n        \n        (e.g. for OAuth)\n        \"\"\"\n        return [\n            ('/login', LoginHandler),\n        ]\n\nclass LocalAuthenticator(Authenticator):\n    \"\"\"Base class for Authenticators that work with local *ix users\n    \n    Checks for local users, and can attempt to create them if they exist.\n    \"\"\"\n    \n    create_system_users = Bool(False, config=True,\n        help=\"\"\"If a user is added that doesn't exist on the system,\n        should I try to create the system user?\n        \"\"\"\n    )\n    add_user_cmd = Command(config=True,\n        help=\"\"\"The command to use for creating users as a list of strings.\n        \n        For each element in the list, the string USERNAME will be replaced with\n        the user's username. The username will also be appended as the final argument.\n        \n        For Linux, the default value is:\n        \n            ['adduser', '-q', '--gecos', '\"\"', '--disabled-password']\n            \n        To specify a custom home directory, set this to:\n        \n            ['adduser', '-q', '--gecos', '\"\"', '--home', '/customhome/USERNAME', '--disabled-password']\n\n        This will run the command:\n\n        adduser -q --gecos \"\" --home /customhome/river --disabled-password river\n        \n        when the user 'river' is created.\n        \"\"\"\n    )\n    def _add_user_cmd_default(self):\n        if sys.platform == 'darwin':\n            raise ValueError(\"I don't know how to create users on OS X\")\n        elif which('pw'):\n            # Probably BSD\n            return ['pw', 'useradd', '-m']\n        else:\n            # This appears to be the Linux non-interactive adduser command:\n            return ['adduser', '-q', '--gecos', '\"\"', '--disabled-password']\n\n    group_whitelist = Set(\n        config=True,\n        help=\"Automatically whitelist anyone in this group.\",\n    )\n\n    def _group_whitelist_changed(self, name, old, new):\n        if self.whitelist:\n            self.log.warn(\n                \"Ignoring username whitelist because group whitelist supplied!\"\n            )\n\n    def check_whitelist(self, username):\n        if self.group_whitelist:\n            return self.check_group_whitelist(username)\n        else:\n            return super().check_whitelist(username)\n\n    def check_group_whitelist(self, username):\n        if not self.group_whitelist:\n            return False\n        for grnam in self.group_whitelist:\n            try:\n                group = getgrnam(grnam)\n            except KeyError:\n                self.log.error('No such group: [%s]' % grnam)\n                continue\n            if username in group.gr_mem:\n                return True\n        return False\n\n    @gen.coroutine\n    def add_user(self, user):\n        \"\"\"Add a new user\n        \n        By default, this just adds the user to the whitelist.\n        \n        Subclasses may do more extensive things,\n        such as adding actual unix users.\n        \"\"\"\n        user_exists = yield gen.maybe_future(self.system_user_exists(user))\n        if not user_exists:\n            if self.create_system_users:\n                yield gen.maybe_future(self.add_system_user(user))\n            else:\n                raise KeyError(\"User %s does not exist.\" % user.name)\n        \n        yield gen.maybe_future(super().add_user(user))\n    \n    @staticmethod\n    def system_user_exists(user):\n        \"\"\"Check if the user exists on the system\"\"\"\n        try:\n            pwd.getpwnam(user.name)\n        except KeyError:\n            return False\n        else:\n            return True\n    \n    def add_system_user(self, user):\n        \"\"\"Create a new *ix user on the system. Works on FreeBSD and Linux, at least.\"\"\"\n        name = user.name\n        cmd = [ arg.replace('USERNAME', name) for arg in self.add_user_cmd ] + [name]\n        self.log.info(\"Creating user: %s\", ' '.join(map(pipes.quote, cmd)))\n        check_call(cmd)\n\n\nclass PAMAuthenticator(LocalAuthenticator):\n    \"\"\"Authenticate local *ix users with PAM\"\"\"\n    encoding = Unicode('utf8', config=True,\n        help=\"\"\"The encoding to use for PAM\"\"\"\n    )\n    service = Unicode('login', config=True,\n        help=\"\"\"The PAM service to use for authentication.\"\"\"\n    )\n    \n    @gen.coroutine\n    def authenticate(self, handler, data):\n        \"\"\"Authenticate with PAM, and return the username if login is successful.\n    \n        Return None otherwise.\n        \"\"\"\n        username = data['username']\n        if not self.check_whitelist(username):\n            return\n        try:\n            pamela.authenticate(username, data['password'], service=self.service)\n        except pamela.PAMError as e:\n            if handler is not None:\n                self.log.warn(\"PAM Authentication failed (@%s): %s\", handler.request.remote_ip, e)\n            else:\n                self.log.warn(\"PAM Authentication failed: %s\", e)\n        else:\n            return username\n    \n    def pre_spawn_start(self, user, spawner):\n        \"\"\"Open PAM session for user\"\"\"\n        try:\n            pamela.open_session(user.name, service=self.service)\n        except pamela.PAMError as e:\n            self.log.warn(\"Failed to open PAM session for %s: %s\", user.name, e)\n    \n    def post_spawn_stop(self, user, spawner):\n        \"\"\"Close PAM session for user\"\"\"\n        try:\n            pamela.close_session(user.name, service=self.service)\n        except pamela.PAMError as e:\n            self.log.warn(\"Failed to close PAM session for %s: %s\", user.name, e)\n    \n", "path": "jupyterhub/auth.py"}]}