Compare commits

...

31 Commits
1.4.2 ... 1.5.1

Author SHA1 Message Date
Min RK
f7903a78d1 release 1.5.1 2022-12-05 13:27:57 +01:00
Erik Sundell
edddacec1d Merge pull request #4234 from minrk/1.x
changelog for 1.5.1
2022-12-01 16:43:44 +01:00
Min RK
c5a33f227f changelog for 1.5.1 2022-11-30 10:56:00 +01:00
Min RK
aaa20e5787 fix flake8 url in pre-commit
repo moved
2022-11-30 10:55:52 +01:00
Min RK
c8ebf1ec90 Merge pull request #4076 from mriedem/1.x-db-fix
Backport PR #3566 to 1.x series
2022-10-12 12:37:22 +02:00
pre-commit-ci[bot]
89745c002b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-10-10 15:44:36 +00:00
Matt Riedemann
3e0ee49ce8 Upgrade black to 22.3.0
Partial cherry-pick of f124f06c2 needed because of
https://github.com/psf/black/issues/2964.
2022-10-10 10:43:36 -05:00
pre-commit-ci[bot]
bbbeffb443 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-10-10 13:49:36 +00:00
Min RK
c29a5ca4ce finish up db rollback checks
- move catch_db_error to utils
- tidy catch/propagate errors in prepare, get_current_user

(cherry picked from commit 3bcc542e27)

Conflicts:
  jupyterhub/handlers/base.py

NOTE(mriedem): The conflict is due to e6845a68f not being
in 1.5.0.
2022-10-10 08:47:00 -05:00
SHAHN3
f5182fe349 add explicit db rollback
add context manager/decorator for db rollback

add db rollback in top level prepare method

Co-authored-by: Sarath Babu <sbreached@gmail.com>
(cherry picked from commit 044fb23a70)
2022-10-10 08:42:42 -05:00
Min RK
bf73e6f7b7 fix 1.5.0 link in changelog 2021-11-04 13:56:40 +01:00
Min RK
e2631b302a import commands from setuptools
importing build_py from distutils breaks in setuptools 58
2021-11-04 13:55:41 +01:00
Min RK
0d89241c9f release 1.5.0 2021-11-04 13:22:53 +01:00
Min RK
5ac9e7f73a Merge branch 'fix-set-cookie' into 1.4.x
Prepare to release 1.5.0

Fixes GHSA-cw7p-q79f-m2v7
2021-11-04 13:21:52 +01:00
Min RK
9672b534ec changelog for 1.5.0 2021-11-03 16:16:48 +01:00
Min RK
254365716d jupyterlab: don't use $JUPYTERHUB_API_TOKEN in PageConfig.token 2021-11-03 15:52:00 +01:00
Min RK
dcac8c4efe Revert "store tokens passed via url or header, not only url."
This reverts commit 53c3201c17.

Only tokens in URLs should be persisted in cookies.
Tokens in headers should not have any effect on cookies.
2021-11-03 15:52:00 +01:00
Min RK
0611169dea Merge pull request #3677 from minrk/doc-requirements-1x
1.4.x: update doc requirements
2021-11-02 10:38:16 +01:00
Min RK
a432fa3bb6 Merge pull request #3676 from manics/1-use_legacy_stopped_server_status_code
use_legacy_stopped_server_status_code: use 1.* language
2021-11-02 10:35:09 +01:00
Min RK
44141ae025 1.4.x: update doc requirements
pin down docutils, unpin autodoc-traits
2021-11-02 10:35:04 +01:00
Simon Li
04ae25d2c2 use_legacy_stopped_server_status_code: use 1.* language
Also fixes the JupyterHub 2.0 default: will be False not True
2021-11-01 22:14:59 +00:00
Min RK
69a1e97fbe Merge pull request #3639 from yuvipanda/404-1.4.x
Backport #3636 to 1.4.x
2021-10-06 16:08:00 +02:00
YuviPanda
eb0c6514af Set use_legacy_stopped_server_status_code to True for 1.4.x 2021-10-06 17:25:10 +05:30
YuviPanda
d03fc8c531 Update tests that were looking for 503s 2021-10-05 20:19:59 +05:30
YuviPanda
1c8dce533b Preserve older 503 behavior behind a flag 2021-10-05 20:19:59 +05:30
YuviPanda
bbfbc47bb3 Use 424 rather than 404 to indicate non-running server
404 is also used to identify that a particular resource
(like a kernel or terminal) is not present, maybe because
it is deleted. That comes from the notebook server, while
here we are responding from JupyterHub. Saying that the
user server they are trying to request the resource (kernel, etc)
from does not exist seems right.
2021-10-05 20:19:59 +05:30
YuviPanda
be6ec28dab Fail suspected API requests with 404, not 503
Non-running user servers making requests is a fairly
common occurance - user servers get culled while their
browser tabs are left open. So we now have a background level
of 503s responses on the hub *all* the time, making it
very difficult to detect *real* 503s, which should ideally
be closely monitored and alerted on.

I *think* 404 is a more appropriate response, as the resource
(API) being requested is no longer present.
2021-10-05 20:19:59 +05:30
Min RK
bd3a215c9e Merge pull request #3580 from meeseeksmachine/auto-backport-of-pr-3552-on-1.4.x
Backport PR #3552 on branch 1.4.x (Add expiration date dropdown to Token page)
2021-08-23 12:08:56 +02:00
Min RK
3783a1bc6c Backport PR #3552: Add expiration date dropdown to Token page 2021-08-23 09:42:15 +00:00
Min RK
7b0f29b340 Merge pull request #3579 from meeseeksmachine/auto-backport-of-pr-3488-on-1.4.x
Backport PR #3488 on branch 1.4.x (Support auto login when used as a OAuth2 provider)
2021-08-23 10:32:15 +02:00
Min RK
f63e810dfe Backport PR #3488: Support auto login when used as a OAuth2 provider 2021-08-20 08:30:04 +00:00
20 changed files with 227 additions and 52 deletions

View File

@@ -4,14 +4,14 @@ repos:
hooks:
- id: reorder-python-imports
- repo: https://github.com/psf/black
rev: 20.8b1
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.2.1
hooks:
- id: prettier
- repo: https://gitlab.com/pycqa/flake8
- repo: https://github.com/pycqa/flake8
rev: "3.8.4"
hooks:
- id: flake8

View File

@@ -1,9 +1,8 @@
-r ../requirements.txt
alabaster_jupyterhub
# Temporary fix of #3021. Revert back to released autodoc-traits when
# 0.1.0 released.
https://github.com/jupyterhub/autodoc-traits/archive/d22282c1c18c6865436e06d8b329c06fe12a07f8.zip
autodoc-traits
docutils<0.18
pydata-sphinx-theme
pytablewriter>=0.56
recommonmark>=0.6

View File

@@ -3,7 +3,7 @@ swagger: "2.0"
info:
title: JupyterHub
description: The REST API for JupyterHub
version: 1.4.0
version: 1.5.1
license:
name: BSD-3-Clause
schemes: [http, https]

View File

@@ -6,6 +6,57 @@ command line for details.
## [Unreleased]
## 1.5
JupyterHub 1.5 is a **security release**,
fixing a vulnerability [ghsa-cw7p-q79f-m2v7][] where JupyterLab users
with multiple tabs open could fail to logout completely,
leaving their browser with valid credentials until they logout again.
A few fully backward-compatible features have been backported from 2.0.
[ghsa-cw7p-q79f-m2v7]: https://github.com/jupyterhub/jupyterhub/security/advisories/GHSA-cw7p-q79f-m2v7
### 1.5.1 2022-12-01
This is a patch release, improving db resiliency when certain errors occur, without requiring a jupyterhub restart.
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.5.0...1.5.1))
#### Merged PRs
- Backport db rollback fixes to 1.x [#4076](https://github.com/jupyterhub/jupyterhub/pull/4076) ([@mriedem](https://github.com/mriedem), [@minrk](https://github.com/minrk)), [@nsshah1288](https://github.com/nsshah1288)
#### Contributors to this release
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2022-09-09&to=2022-11-30&type=c))
[@mriedem](https://github.com/mriedem) | [@minrk](https://github.com/minrk) | [@nsshah1288](https://github.com/nsshah1288)
### [1.5.0] 2021-11-04
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0))
#### New features added
- Backport #3636 to 1.4.x (opt-in support for JupyterHub.use_legacy_stopped_server_status_code) [#3639](https://github.com/jupyterhub/jupyterhub/pull/3639) ([@yuvipanda](https://github.com/yuvipanda))
- Backport PR #3552 on branch 1.4.x (Add expiration date dropdown to Token page) [#3580](https://github.com/jupyterhub/jupyterhub/pull/3580) ([@meeseeksmachine](https://github.com/meeseeksmachine))
- Backport PR #3488 on branch 1.4.x (Support auto login when used as a OAuth2 provider) [#3579](https://github.com/jupyterhub/jupyterhub/pull/3579) ([@meeseeksmachine](https://github.com/meeseeksmachine))
#### Maintenance and upkeep improvements
- 1.4.x: update doc requirements [#3677](https://github.com/jupyterhub/jupyterhub/pull/3677) ([@minrk](https://github.com/minrk))
#### Documentation improvements
- use_legacy_stopped_server_status_code: use 1.\* language [#3676](https://github.com/jupyterhub/jupyterhub/pull/3676) ([@manics](https://github.com/manics))
#### Contributors to this release
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2021-07-16&to=2021-11-03&type=c))
[@choldgraf](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Acholdgraf+updated%3A2021-07-16..2021-11-03&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2021-07-16..2021-11-03&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2021-07-16..2021-11-03&type=Issues) | [@meeseeksmachine](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ameeseeksmachine+updated%3A2021-07-16..2021-11-03&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2021-07-16..2021-11-03&type=Issues) | [@support](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asupport+updated%3A2021-07-16..2021-11-03&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Awelcome+updated%3A2021-07-16..2021-11-03&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Ayuvipanda+updated%3A2021-07-16..2021-11-03&type=Issues)
## 1.4
JupyterHub 1.4 is a small release, with several enhancements, bug fixes,
@@ -1097,7 +1148,8 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers.
First preview release
[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.4.1...HEAD
[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.5.0...HEAD
[1.5.0]: https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0
[1.4.2]: https://github.com/jupyterhub/jupyterhub/compare/1.4.1...1.4.2
[1.4.1]: https://github.com/jupyterhub/jupyterhub/compare/1.4.0...1.4.1
[1.4.0]: https://github.com/jupyterhub/jupyterhub/compare/1.3.0...1.4.0

View File

@@ -10,7 +10,7 @@ from jupyter_client.localinterfaces import public_ips
def create_dir_hook(spawner):
""" Create directory """
"""Create directory"""
username = spawner.user.name # get the username
volume_path = os.path.join('/volumes/jupyterhub', username)
if not os.path.exists(volume_path):
@@ -20,7 +20,7 @@ def create_dir_hook(spawner):
def clean_dir_hook(spawner):
""" Delete directory """
"""Delete directory"""
username = spawner.user.name # get the username
temp_path = os.path.join('/volumes/jupyterhub', username, 'temp')
if os.path.exists(temp_path) and os.path.isdir(temp_path):

View File

@@ -4,8 +4,8 @@
version_info = (
1,
4,
2,
5,
1,
"", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing for beta/rc/stable releases
)

View File

@@ -222,6 +222,14 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
# default: require confirmation
return True
def get_login_url(self):
"""
Support automatically logging in when JupyterHub is used as auth provider
"""
if self.authenticator.auto_login_oauth2_authorize:
return self.authenticator.login_url(self.hub.base_url)
return super().get_login_url()
@web.authenticated
async def get(self):
"""GET /oauth/authorization

View File

@@ -42,7 +42,7 @@ class GroupListAPIHandler(_GroupAPIHandler):
@admin_only
async def post(self):
"""POST creates Multiple groups """
"""POST creates Multiple groups"""
model = self.get_json_body()
if not model or not isinstance(model, dict) or not model.get('groups'):
raise web.HTTPError(400, "Must specify at least one group to create")

View File

@@ -5,7 +5,6 @@
import asyncio
import atexit
import binascii
import json
import logging
import os
import re
@@ -90,6 +89,7 @@ from .pagination import Pagination
from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix, Command, EntryPointType, Callable
from .utils import (
catch_db_error,
maybe_future,
url_path_join,
print_stacks,
@@ -369,7 +369,7 @@ class JupyterHub(Application):
even if your Hub authentication is still valid.
If your Hub authentication is valid,
logging in may be a transparent redirect as you refresh the page.
This does not affect JupyterHub API tokens in general,
which do not expire by default.
Only tokens issued during the oauth flow
@@ -862,7 +862,7 @@ class JupyterHub(Application):
"/",
help="""
The routing prefix for the Hub itself.
Override to send only a subset of traffic to the Hub.
Default is to use the Hub as the default route for all requests.
@@ -874,7 +874,7 @@ class JupyterHub(Application):
may want to handle these events themselves,
in which case they can register their own default target with the proxy
and set e.g. `hub_routespec = /hub/` to serve only the hub's own pages, or even `/hub/api/` for api-only operation.
Note: hub_routespec must include the base_url, if any.
.. versionadded:: 1.4
@@ -1448,7 +1448,7 @@ class JupyterHub(Application):
Can be a Unicode string (e.g. '/hub/home') or a callable based on the handler object:
::
def default_url_fn(handler):
user = handler.current_user
if user and user.admin:
@@ -1476,6 +1476,26 @@ class JupyterHub(Application):
""",
).tag(config=True)
use_legacy_stopped_server_status_code = Bool(
True,
help="""
Return 503 rather than 424 when request comes in for a non-running server.
Prior to JupyterHub 2.0, this returns a 503 when any request comes in for
a user server that is currently not running. By default, JupyterHub 2.0
will return a 424 - this makes operational metric dashboards more useful.
JupyterLab < 3.2 expected the 503 to know if the user server is no longer
running, and prompted the user to start their server. Set this config to
true to retain the old behavior, so JupyterLab < 3.2 can continue to show
the appropriate UI when the user server is stopped.
This option will default to False in JupyterHub 2.0, and be removed in a
future release.
""",
config=True,
)
def init_handlers(self):
h = []
# load handlers from the authenticator
@@ -1980,6 +2000,7 @@ class JupyterHub(Application):
# purge expired tokens hourly
purge_expired_tokens_interval = 3600
@catch_db_error
def purge_expired_tokens(self):
"""purge all expiring token objects from the database
@@ -1995,7 +2016,7 @@ class JupyterHub(Application):
await self._add_tokens(self.service_tokens, kind='service')
await self._add_tokens(self.api_tokens, kind='user')
self.purge_expired_tokens()
await self.purge_expired_tokens()
# purge expired tokens hourly
# we don't need to be prompt about this
# because expired tokens cannot be used anyway
@@ -2643,6 +2664,7 @@ class JupyterHub(Application):
with open(self.config_file, mode='w') as f:
f.write(config_text)
@catch_db_error
async def update_last_activity(self):
"""Update User.last_activity timestamps from the proxy"""
routes = await self.proxy.get_all_routes()

View File

@@ -646,6 +646,26 @@ class Authenticator(LoggingConfigurable):
""",
)
auto_login_oauth2_authorize = Bool(
False,
config=True,
help="""
Automatically begin login process for OAuth2 authorization requests
When another application is using JupyterHub as OAuth2 provider, it
sends users to `/hub/api/oauth2/authorize`. If the user isn't logged
in already, and auto_login is not set, the user will be dumped on the
hub's home page, without any context on what to do next.
Setting this to true will automatically redirect users to login if
they aren't logged in *only* on the `/hub/api/oauth2/authorize`
endpoint.
.. versionadded:: 1.5
""",
)
def login_url(self, base_url):
"""Override this when registering a custom login handler
@@ -952,8 +972,8 @@ class PAMAuthenticator(LocalAuthenticator):
help="""
Whether to check the user's account status via PAM during authentication.
The PAM account stack performs non-authentication based account
management. It is typically used to restrict/permit access to a
The PAM account stack performs non-authentication based account
management. It is typically used to restrict/permit access to a
service and this step is needed to access the host's user access control.
Disabling this can be dangerous as authenticated but unauthorized users may

View File

@@ -81,9 +81,14 @@ class BaseHandler(RequestHandler):
"""
try:
await self.get_current_user()
except Exception:
self.log.exception("Failed to get current user")
except Exception as e:
# ensure get_current_user is never called again for this handler,
# since it failed
self._jupyterhub_user = None
self.log.exception("Failed to get current user")
if isinstance(e, SQLAlchemyError):
self.log.error("Rolling back session due to database error")
self.db.rollback()
return await maybe_future(super().prepare())
@@ -426,7 +431,8 @@ class BaseHandler(RequestHandler):
except Exception:
# don't let errors here raise more than once
self._jupyterhub_user = None
self.log.exception("Error getting current user")
# but still raise, which will get handled in .prepare()
raise
return self._jupyterhub_user
@property
@@ -1330,7 +1336,7 @@ class UserUrlHandler(BaseHandler):
**Changed Behavior as of 1.0** This handler no longer triggers a spawn. Instead, it checks if:
1. server is not active, serve page prompting for spawn (status: 503)
1. server is not active, serve page prompting for spawn (status: 424)
2. server is ready (This shouldn't happen! Proxy isn't updated yet. Wait a bit and redirect.)
3. server is active, redirect to /hub/spawn-pending to monitor launch progress
(will redirect back when finished)
@@ -1349,7 +1355,14 @@ class UserUrlHandler(BaseHandler):
self.log.warning(
"Failing suspected API request to not-running server: %s", self.request.path
)
self.set_status(503)
# If we got here, the server is not running. To differentiate
# that the *server* itself is not running, rather than just the particular
# resource *in* the server is not found, we return a 424 instead of a 404.
# We allow retaining the old behavior to support older JupyterLab versions
self.set_status(
424 if not self.app.use_legacy_stopped_server_status_code else 503
)
self.set_header("Content-Type", "application/json")
spawn_url = urlparse(self.request.full_url())._replace(query="")
@@ -1492,14 +1505,10 @@ class UserUrlHandler(BaseHandler):
# if request is expecting JSON, assume it's an API request and fail with 503
# because it won't like the redirect to the pending page
if (
get_accepted_mimetype(
self.request.headers.get('Accept', ''),
choices=['application/json', 'text/html'],
)
== 'application/json'
or 'api' in user_path.split('/')
):
if get_accepted_mimetype(
self.request.headers.get('Accept', ''),
choices=['application/json', 'text/html'],
) == 'application/json' or 'api' in user_path.split('/'):
self._fail_api_request(user_name, server_name)
return
@@ -1514,15 +1523,17 @@ class UserUrlHandler(BaseHandler):
self.redirect(pending_url, status=303)
return
# if we got here, the server is not running
# serve a page prompting for spawn and 503 error
# visiting /user/:name no longer triggers implicit spawn
# without explicit user action
# If we got here, the server is not running. To differentiate
# that the *server* itself is not running, rather than just the particular
# page *in* the server is not found, we return a 424 instead of a 404.
# We allow retaining the old behavior to support older JupyterLab versions
spawn_url = url_concat(
url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name),
{"next": self.request.uri},
)
self.set_status(503)
self.set_status(
424 if not self.app.use_legacy_stopped_server_status_code else 503
)
auth_state = await user.get_auth_state()
html = await self.render_template(
@@ -1579,7 +1590,7 @@ class UserUrlHandler(BaseHandler):
if redirects:
self.log.warning("Redirect loop detected on %s", self.request.uri)
# add capped exponential backoff where cap is 10s
await asyncio.sleep(min(1 * (2 ** redirects), 10))
await asyncio.sleep(min(1 * (2**redirects), 10))
# rewrite target url with new `redirects` query value
url_parts = urlparse(target)
query_parts = parse_qs(url_parts.query)

View File

@@ -223,7 +223,7 @@ class User(Base):
class Spawner(Base):
""""State about a Spawner"""
""" "State about a Spawner"""
__tablename__ = 'spawners'

View File

@@ -927,8 +927,8 @@ class HubAuthenticated(object):
self._hub_auth_user_cache = None
raise
# store tokens passed via url or header in a cookie for future requests
url_token = self.hub_auth.get_token(self)
# store ?token=... tokens passed via url in a cookie for future requests
url_token = self.get_argument('token', '')
if (
user_model
and url_token

View File

@@ -452,7 +452,7 @@ class SingleUserNotebookAppMixin(Configurable):
i,
RETRIES,
)
await asyncio.sleep(min(2 ** i, 16))
await asyncio.sleep(min(2**i, 16))
else:
break
else:
@@ -675,6 +675,18 @@ class SingleUserNotebookAppMixin(Configurable):
orig_loader = env.loader
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
def load_server_extensions(self):
# Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect
r = super().load_server_extensions()
# clear the token in PageConfig at this step
# so that cookie auth is used
# FIXME: in the future,
# it would probably make sense to set page_config.token to the token
# from the current request.
if 'page_config_data' in self.web_app.settings:
self.web_app.settings['page_config_data']['token'] = ''
return r
def detect_base_package(App):
"""Detect the base package for an App class

View File

@@ -1008,6 +1008,13 @@ async def test_server_not_running_api_request(app):
assert " /user/bees" in message
async def test_server_not_running_api_request_legacy_status(app):
app.use_legacy_stopped_server_status_code = False
cookies = await app.login_user("bees")
r = await get_page("user/bees/api/status", app, hub=False, cookies=cookies)
assert r.status_code == 424
async def test_metrics_no_auth(app):
r = await get_page("metrics", app)
assert r.status_code == 403

View File

@@ -4,9 +4,9 @@
import asyncio
import concurrent.futures
import errno
import functools
import hashlib
import inspect
import os
import random
import secrets
import socket
@@ -22,6 +22,7 @@ from hmac import compare_digest
from operator import itemgetter
from async_generator import aclosing
from sqlalchemy.exc import SQLAlchemyError
from tornado import ioloop
from tornado import web
from tornado.httpclient import AsyncHTTPClient
@@ -642,3 +643,21 @@ def get_accepted_mimetype(accept_header, choices=None):
else:
return mime
return None
def catch_db_error(f):
"""Catch and rollback database errors"""
@functools.wraps(f)
async def catching(self, *args, **kwargs):
try:
r = f(self, *args, **kwargs)
if inspect.isawaitable(r):
r = await r
except SQLAlchemyError:
self.log.exception("Rolling back session due to database error")
self.db.rollback()
else:
return r
return catching

View File

@@ -12,8 +12,11 @@ import shutil
import sys
from subprocess import check_call
from setuptools import Command
from setuptools import setup
from setuptools.command.bdist_egg import bdist_egg
from setuptools.command.build_py import build_py
from setuptools.command.sdist import sdist
v = sys.version_info
@@ -132,14 +135,9 @@ setup_args = dict(
)
# ---------------------------------------------------------------------------
# custom distutils commands
# custom setuptools commands
# ---------------------------------------------------------------------------
# imports here, so they are after setuptools import if there was one
from distutils.cmd import Command
from distutils.command.build_py import build_py
from distutils.command.sdist import sdist
def mtime(path):
"""shorthand for mtime"""

View File

@@ -20,9 +20,14 @@ require(["jquery", "jhapi", "moment"], function ($, JHAPI, moment) {
if (!note.length) {
note = "Requested via token page";
}
var expiration_seconds =
parseInt($("#token-expiration-seconds").val()) || null;
api.request_token(
user,
{ note: note },
{
note: note,
expires_in: expiration_seconds,
},
{
success: function (reply) {
$("#token-result").text(reply.token);

View File

@@ -19,6 +19,20 @@
<small id="note-note" class="form-text text-muted">
This note will help you keep track of what your tokens are for.
</small>
<br><br>
<label for="token-expiration-seconds">Token expires</label>
{% block expiration_options %}
<select id="token-expiration-seconds"
class="form-control">
<option value="3600">1 Day</option>
<option value="86400">1 Week</option>
<option value="604800">1 Month</option>
<option value="" selected="selected">Never</option>
</select>
{% endblock expiration_options %}
<small id="note-expires-at" class="form-text text-muted">
You can configure when your token will be expired.
</small>
</div>
</form>
</div>
@@ -56,6 +70,7 @@
<td>Note</td>
<td>Last used</td>
<td>Created</td>
<td>Expires at</td>
</tr>
</thead>
<tbody>
@@ -77,6 +92,13 @@
N/A
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if token.expires_at -%}
{{ token.expires_at.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
</td>

View File

@@ -26,8 +26,8 @@ import os
import pipes
import shutil
from contextlib import contextmanager
from distutils.version import LooseVersion as V
from distutils.version import LooseVersion as V
from invoke import run as invoke_run
from invoke import task