Repository: jupyterhub/nativeauthenticator (language: python)

Issue:
Replaced deprecated admin_only with new needs_scope
JupyterHub 2.0 deprecated the admin_only decorator in jupyterhub/jupyterhub#3659. Per the deprecation warning, I'm replacing admin_only with the new needs_scope decorator, using the scope 'admin:users'. In testing, this produced 403 errors for non-admin users as expected. This should address https://github.com/jupyterhub/nativeauthenticator/issues/157
admin_only decorator deprecated in jupyterhub 2.0
### Bug description
I deployed Zero to Jupyterhub recently and the current k8s-hub docker image includes jupyterhub-nativeauthenticator   1.0.5 and jupyterhub 2.0.0b3. These seem to not be compatible due to major code changes in jupyterhub 2.0. 

#### Expected behaviour
I expected to be able to "import nativeauthenticator" without errors.

#### Actual behaviour
Importing nativeauthenticator produces the error `ImportError: cannot import name 'admin_only' from 'jupyterhub.utils'`

### How to reproduce
```
$ docker run -it --entrypoint /bin/bash jupyterhub/k8s-hub:1.1.3-n134.h03e4a766
jovyan@7a9e131f6fbd:/srv/jupyterhub$ pip list | grep jupyterhub
jupyterhub                       2.0.0b3
jupyterhub-firstuseauthenticator 0.14.1
jupyterhub-hmacauthenticator     1.0
jupyterhub-idle-culler           1.2.1
jupyterhub-kubespawner           1.1.1
jupyterhub-ldapauthenticator     1.3.2
jupyterhub-ltiauthenticator      1.2.0
jupyterhub-nativeauthenticator   1.0.5
jupyterhub-tmpauthenticator      0.6
jovyan@7a9e131f6fbd:/srv/jupyterhub$ python3
Python 3.8.10 (default, Sep 28 2021, 16:10:42)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import nativeauthenticator
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/dist-packages/nativeauthenticator/__init__.py", line 4, in <module>
    from nativeauthenticator.nativeauthenticator import NativeAuthenticator
  File "/usr/local/lib/python3.8/dist-packages/nativeauthenticator/nativeauthenticator.py", line 16, in <module>
    from .handlers import (
  File "/usr/local/lib/python3.8/dist-packages/nativeauthenticator/handlers.py", line 7, in <module>
    from jupyterhub.utils import admin_only
ImportError: cannot import name 'admin_only' from 'jupyterhub.utils' (/usr/local/lib/python3.8/dist-packages/jupyterhub/utils.py)
```

### Your personal set up
Using the Z2JH Docker image jupyterhub/k8s-hub:1.1.3-n134.h03e4a766



Discussion / hints:
Thanks for submitting your first pull request! You are awesome! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please make sure you followed the pull request template, as this will help us review your contribution more quickly.
![welcome](https://raw.githubusercontent.com/jupyterhub/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also a intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:

Merging this would necessitate bumping the JupyterHub dependency (see all the failing tests), and I don't think we're quite ready for that yet. I'm not sure entirely what a good point in time for that would be, ideally (but I don't know if that's feasible) we'd have a solution that works with both (try-catch or something).
I agree with @lambdaTotoro that it would be a bit troublesome if this authenticator, after adjusted like this, would only run on jupyterhub 2+. I think a try/catch of trying to import the needs_scope decorator early on would be a very appreciated PR though!

I'm thinking something like...

```python
from functools import partial

try:
    from jupyterhub.scopes import needs_scope
    admin_users_scope = partial(needs_scope, "admin:users")
except ImportError:
    from jupyterhub.utils import admin_only as admin_users_scope


class Something:
    @admin_users_scope()
    def some_function():
        pass
```

This doesn't really work though hehe... I get `TypeError: decorator() missing 1 required positional argument: 'method'` when using JupyterHub 1. But something along those lines.

I'll try setup tests against JupyterHub pre-releases!
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! :hugs:
<br>If you haven't done so already, check out [Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
![welcome](https://raw.githubusercontent.com/jupyterhub/.github/master/images/welcome.jpg)
You can meet the other [Jovyans](https://jupyter.readthedocs.io/en/latest/community/content-community.html?highlight=jovyan#what-is-a-jovyan) by joining our [Discourse forum](http://discourse.jupyter.org/). There is also an intro thread there where you can stop by and say Hi! :wave:
<br>Welcome to the Jupyter community! :tada:

Thanks for letting me know, I will see how we can become compatible with the new roles system in `jupyterhub >= 2.0`
This issue has been mentioned on **Jupyter Community Forum**. There might be relevant details there:

https://discourse.jupyter.org/t/nativeauthenticator-broken-in-jupyterhub-k8s-hub-1-1-3-x-due-to-jupyterhub-2-0-0b3-bump/11335/2

There's no reason this decorator needs to be removed instead of just deprecated, so we'll do that for 2.0: https://github.com/jupyterhub/jupyterhub/pull/3659
That still needs replacing then, only less urgently. We'll just use this issue to track that. 

Fix patch:
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 45839ff..d1e341b 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -1,5 +1,5 @@
 # This is a GitHub workflow defining a set of jobs with a set of steps.
-# ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
+# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
 #
 # Publish to PyPI on push of version like tags
 #
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/nativeauthenticator/handlers.py b/nativeauthenticator/handlers.py
index fd347d6..ea5a5eb 100644
--- a/nativeauthenticator/handlers.py
+++ b/nativeauthenticator/handlers.py
@@ -4,7 +4,12 @@
 from jinja2 import ChoiceLoader, FileSystemLoader
 from jupyterhub.handlers import BaseHandler
 from jupyterhub.handlers.login import LoginHandler
-from jupyterhub.utils import admin_only
+try:
+    from jupyterhub.scopes import needs_scope
+    admin_users_scope = needs_scope("admin:users")
+except ImportError:
+    from jupyterhub.utils import admin_only
+    admin_users_scope = admin_only
 
 from tornado import web
 from tornado.escape import url_escape
@@ -145,7 +150,7 @@ async def post(self):
 
 class AuthorizationHandler(LocalBase):
     """Render the sign in page."""
-    @admin_only
+    @admin_users_scope
     async def get(self):
         html = await self.render_template(
             'autorization-area.html',
@@ -156,7 +161,7 @@ async def get(self):
 
 
 class ChangeAuthorizationHandler(LocalBase):
-    @admin_only
+    @admin_users_scope
     async def get(self, slug):
         UserInfo.change_authorization(self.db, slug)
         self.redirect(self.hub.base_url + 'authorize#' + slug)
@@ -256,7 +261,7 @@ async def post(self):
 class ChangePasswordAdminHandler(LocalBase):
     """Render the reset password page."""
 
-    @admin_only
+    @admin_users_scope
     async def get(self, user_name):
         if not self.authenticator.user_exists(user_name):
             raise web.HTTPError(404)
@@ -266,7 +271,7 @@ async def get(self, user_name):
         )
         self.finish(html)
 
-    @admin_only
+    @admin_users_scope
     async def post(self, user_name):
         new_password = self.get_body_argument('password', strip=False)
         self.authenticator.change_password(user_name, new_password)
@@ -334,7 +339,7 @@ async def post(self):
 class DiscardHandler(LocalBase):
     """Discard a user from database"""
 
-    @admin_only
+    @admin_users_scope
     async def get(self, user_name):
         user = self.authenticator.get_user(user_name)
         if user is not None:


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 2e27497..546b524 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,5 +1,5 @@
 # This is a GitHub workflow defining a set of jobs with a set of steps.
-# ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
+# ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
 #
 name: Test
 
@@ -35,20 +35,27 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - python-version: 3.6
-          - python-version: 3.7
-          - python-version: 3.8
-          - python-version: 3.9
+          - python-version: "3.6"
+            jupyterhub-version: "1.3.*"
+          - python-version: "3.7"
+            jupyterhub-version: "1.4.*"
+          - python-version: "3.8"
+            jupyterhub-version: "1.*"
+          - python-version: "3.9"
+            jupyterhub-version: "2.*"
+          - python-version: "3.10"
+            jupyterhub-version: "2.*"
 
     steps:
       - uses: actions/checkout@v2
       - uses: actions/setup-python@v2
         with:
-          python-version: "${{ matrix.python-version}}"
+          python-version: "${{ matrix.python-version }}"
 
       - name: Install Python dependencies
         run: |
           pip install --no-cache-dir -r dev-requirements.txt
+          pip install --pre "jupyterhub==${{ matrix.jupyterhub-version }}"
           pip install .
 
       - name: Run tests