mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 13:33:00 +00:00
Merge branch 'main' into group_property_feature
This commit is contained in:
@@ -11,34 +11,34 @@
|
||||
repos:
|
||||
# Autoformat: Python code, syntax patterns are modernized
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.1
|
||||
rev: v2.32.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py36-plus
|
||||
|
||||
# Autoformat: Python code
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.0.1
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
- id: isort
|
||||
|
||||
# Autoformat: Python code
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--target-version=py36]
|
||||
|
||||
# Autoformat: markdown, yaml, javascript (see the file .prettierignore)
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.6.1
|
||||
rev: v2.6.2
|
||||
hooks:
|
||||
- id: prettier
|
||||
|
||||
# Autoformat and linting, misc. details
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
exclude: share/jupyterhub/static/js/admin-react.js
|
||||
|
@@ -59,7 +59,7 @@ JupyterHub also provides a
|
||||
[REST API][]
|
||||
for administration of the Hub and its users.
|
||||
|
||||
[rest api]: https://juptyerhub.readthedocs.io/en/latest/reference/rest-api.html
|
||||
[rest api]: https://jupyterhub.readthedocs.io/en/latest/reference/rest-api.html
|
||||
|
||||
## Installation
|
||||
|
||||
|
@@ -56,13 +56,15 @@ todo_include_todos = False
|
||||
# Set the default role so we can use `foo` instead of ``foo``
|
||||
default_role = 'literal'
|
||||
|
||||
# -- Config -------------------------------------------------------------
|
||||
from jupyterhub.app import JupyterHub
|
||||
from docutils import nodes
|
||||
from sphinx.directives.other import SphinxDirective
|
||||
from contextlib import redirect_stdout
|
||||
from io import StringIO
|
||||
|
||||
from docutils import nodes
|
||||
from sphinx.directives.other import SphinxDirective
|
||||
|
||||
# -- Config -------------------------------------------------------------
|
||||
from jupyterhub.app import JupyterHub
|
||||
|
||||
# create a temp instance of JupyterHub just to get the output of the generate-config
|
||||
# and help --all commands.
|
||||
jupyterhub_app = JupyterHub()
|
||||
|
@@ -7,9 +7,10 @@ to enable testing without administrative privileges.
|
||||
c = get_config() # noqa
|
||||
c.Application.log_level = 'DEBUG'
|
||||
|
||||
from oauthenticator.azuread import AzureAdOAuthenticator
|
||||
import os
|
||||
|
||||
from oauthenticator.azuread import AzureAdOAuthenticator
|
||||
|
||||
c.JupyterHub.authenticator_class = AzureAdOAuthenticator
|
||||
|
||||
c.AzureAdOAuthenticator.client_id = os.getenv("AAD_CLIENT_ID")
|
||||
|
@@ -5,12 +5,9 @@ from urllib.parse import urlparse
|
||||
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import Application
|
||||
from tornado.web import authenticated
|
||||
from tornado.web import RequestHandler
|
||||
from tornado.web import Application, RequestHandler, authenticated
|
||||
|
||||
from jupyterhub.services.auth import HubOAuthCallbackHandler
|
||||
from jupyterhub.services.auth import HubOAuthenticated
|
||||
from jupyterhub.services.auth import HubOAuthCallbackHandler, HubOAuthenticated
|
||||
from jupyterhub.utils import url_path_join
|
||||
|
||||
SCOPE_PREFIX = "custom:grades"
|
||||
|
@@ -5,13 +5,10 @@ so all URLs and requests necessary for OAuth with JupyterHub should be in one pl
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
from tornado import log
|
||||
from tornado import web
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httpclient import HTTPRequest
|
||||
from tornado import log, web
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||
from tornado.httputil import url_concat
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
|
@@ -16,7 +16,6 @@ import time
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -3,9 +3,7 @@ import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
from tornado import escape
|
||||
from tornado import ioloop
|
||||
from tornado import web
|
||||
from tornado import escape, ioloop, web
|
||||
|
||||
from jupyterhub.services.auth import HubAuthenticated
|
||||
|
||||
|
@@ -1,8 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Security
|
||||
from fastapi import status
|
||||
from fastapi import HTTPException, Security, status
|
||||
from fastapi.security import OAuth2AuthorizationCodeBearer
|
||||
from fastapi.security.api_key import APIKeyQuery
|
||||
|
||||
|
@@ -1,14 +1,9 @@
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import Form
|
||||
from fastapi import Request
|
||||
from fastapi import APIRouter, Depends, Form, Request
|
||||
|
||||
from .client import get_client
|
||||
from .models import AuthorizationError
|
||||
from .models import HubApiError
|
||||
from .models import User
|
||||
from .models import AuthorizationError, HubApiError, User
|
||||
from .security import get_current_user
|
||||
|
||||
# APIRouter prefix cannot end in /
|
||||
|
@@ -7,16 +7,10 @@ import os
|
||||
import secrets
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask
|
||||
from flask import make_response
|
||||
from flask import redirect
|
||||
from flask import request
|
||||
from flask import Response
|
||||
from flask import session
|
||||
from flask import Flask, Response, make_response, redirect, request, session
|
||||
|
||||
from jupyterhub.services.auth import HubOAuth
|
||||
|
||||
|
||||
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
|
||||
|
||||
auth = HubOAuth(api_token=os.environ['JUPYTERHUB_API_TOKEN'], cache_max_age=60)
|
||||
|
@@ -10,12 +10,9 @@ from urllib.parse import urlparse
|
||||
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import Application
|
||||
from tornado.web import authenticated
|
||||
from tornado.web import RequestHandler
|
||||
from tornado.web import Application, RequestHandler, authenticated
|
||||
|
||||
from jupyterhub.services.auth import HubOAuthCallbackHandler
|
||||
from jupyterhub.services.auth import HubOAuthenticated
|
||||
from jupyterhub.services.auth import HubOAuthCallbackHandler, HubOAuthenticated
|
||||
from jupyterhub.utils import url_path_join
|
||||
|
||||
|
||||
|
@@ -10,9 +10,7 @@ from urllib.parse import urlparse
|
||||
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import Application
|
||||
from tornado.web import authenticated
|
||||
from tornado.web import RequestHandler
|
||||
from tornado.web import Application, RequestHandler, authenticated
|
||||
|
||||
from jupyterhub.services.auth import HubAuthenticated
|
||||
|
||||
|
@@ -361,7 +361,7 @@ const ServerDashboard = (props) => {
|
||||
name="user_search"
|
||||
placeholder="Search users"
|
||||
aria-label="user-search"
|
||||
value={name_filter}
|
||||
defaultValue={name_filter}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</Col>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { HashRouter, Switch } from "react-router-dom";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
@@ -508,15 +509,15 @@ test("Search for user calls updateUsers with name filter", async () => {
|
||||
|
||||
let search = screen.getByLabelText("user-search");
|
||||
|
||||
fireEvent.change(search, { target: { value: "a" } });
|
||||
clock.tick(400);
|
||||
expect(mockUpdateUsers.mock.calls).toHaveLength(2);
|
||||
expect(mockUpdateUsers.mock.calls[1][2]).toEqual("a");
|
||||
userEvent.type(search, "a");
|
||||
expect(search.value).toEqual("a");
|
||||
|
||||
fireEvent.change(search, { target: { value: "ab" } });
|
||||
clock.tick(400);
|
||||
expect(mockUpdateUsers.mock.calls).toHaveLength(3);
|
||||
expect(mockUpdateUsers.mock.calls[2][2]).toEqual("ab");
|
||||
expect(mockUpdateUsers.mock.calls[1][2]).toEqual("a");
|
||||
expect(mockUpdateUsers.mock.calls).toHaveLength(2);
|
||||
|
||||
userEvent.type(search, "b");
|
||||
expect(search.value).toEqual("ab");
|
||||
clock.tick(400);
|
||||
expect(mockUpdateUsers.mock.calls[2][2]).toEqual("ab");
|
||||
expect(mockUpdateUsers.mock.calls).toHaveLength(3);
|
||||
});
|
||||
|
@@ -1,2 +1 @@
|
||||
from ._version import __version__
|
||||
from ._version import version_info
|
||||
from ._version import __version__, version_info
|
||||
|
@@ -4,7 +4,7 @@
|
||||
def get_data_files():
|
||||
"""Walk up until we find share/jupyterhub"""
|
||||
import sys
|
||||
from os.path import join, abspath, dirname, exists, split
|
||||
from os.path import abspath, dirname, exists, join, split
|
||||
|
||||
path = abspath(dirname(__file__))
|
||||
starting_points = [path]
|
||||
|
154
jupyterhub/_memoize.py
Normal file
154
jupyterhub/_memoize.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Utilities for memoization
|
||||
|
||||
Note: a memoized function should always return an _immutable_
|
||||
result to avoid later modifications polluting cached results.
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
|
||||
|
||||
class DoNotCache:
|
||||
"""Wrapper to return a result without caching it.
|
||||
|
||||
In a function decorated with `@lru_cache_key`:
|
||||
|
||||
return DoNotCache(result)
|
||||
|
||||
is equivalent to:
|
||||
|
||||
return result # but don't cache it!
|
||||
"""
|
||||
|
||||
def __init__(self, result):
|
||||
self.result = result
|
||||
|
||||
|
||||
class LRUCache:
|
||||
"""A simple Least-Recently-Used (LRU) cache with a max size"""
|
||||
|
||||
def __init__(self, maxsize=1024):
|
||||
self._cache = OrderedDict()
|
||||
self.maxsize = maxsize
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._cache
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Get an item from the cache"""
|
||||
if key in self._cache:
|
||||
# cache hit, bump to front of the queue for LRU
|
||||
result = self._cache[key]
|
||||
self._cache.move_to_end(key)
|
||||
return result
|
||||
return default
|
||||
|
||||
def set(self, key, value):
|
||||
"""Store an entry in the cache
|
||||
|
||||
Purges oldest entry if cache is full
|
||||
"""
|
||||
self._cache[key] = value
|
||||
# cache is full, purge oldest entry
|
||||
if len(self._cache) > self.maxsize:
|
||||
self._cache.popitem(last=False)
|
||||
|
||||
__getitem__ = get
|
||||
__setitem__ = set
|
||||
|
||||
|
||||
def lru_cache_key(key_func, maxsize=1024):
|
||||
"""Like functools.lru_cache, but takes a custom key function,
|
||||
as seen in sorted(key=func).
|
||||
|
||||
Useful for non-hashable arguments which have a known hashable equivalent (e.g. sets, lists),
|
||||
or mutable objects where only immutable fields might be used
|
||||
(e.g. User, where only username affects output).
|
||||
|
||||
For safety: Cached results should always be immutable,
|
||||
such as using `frozenset` instead of mutable `set`.
|
||||
|
||||
Example:
|
||||
|
||||
@lru_cache_key(lambda user: user.name)
|
||||
def func_user(user):
|
||||
# output only varies by name
|
||||
|
||||
Args:
|
||||
key (callable):
|
||||
Should have the same signature as the decorated function.
|
||||
Returns a hashable key to use in the cache
|
||||
maxsize (int):
|
||||
The maximum size of the cache.
|
||||
"""
|
||||
|
||||
def cache_func(func):
|
||||
cache = LRUCache(maxsize=maxsize)
|
||||
# the actual decorated function:
|
||||
@wraps(func)
|
||||
def cached(*args, **kwargs):
|
||||
cache_key = key_func(*args, **kwargs)
|
||||
if cache_key in cache:
|
||||
# cache hit
|
||||
return cache[cache_key]
|
||||
else:
|
||||
# cache miss, call function and cache result
|
||||
result = func(*args, **kwargs)
|
||||
if isinstance(result, DoNotCache):
|
||||
# DoNotCache prevents caching
|
||||
result = result.result
|
||||
else:
|
||||
cache[cache_key] = result
|
||||
return result
|
||||
|
||||
return cached
|
||||
|
||||
return cache_func
|
||||
|
||||
|
||||
class FrozenDict(dict):
|
||||
"""A frozen dictionary subclass
|
||||
|
||||
Immutable and hashable, so it can be used as a cache key
|
||||
|
||||
Values will be frozen with `.freeze(value)`
|
||||
and must be hashable after freezing.
|
||||
|
||||
Not rigorous, but enough for our purposes.
|
||||
"""
|
||||
|
||||
_hash = None
|
||||
|
||||
def __init__(self, d):
|
||||
dict_set = dict.__setitem__
|
||||
for key, value in d.items():
|
||||
dict.__setitem__(self, key, self._freeze(value))
|
||||
|
||||
def _freeze(self, item):
|
||||
"""Make values of a dict hashable
|
||||
- list, set -> frozenset
|
||||
- dict -> recursive _FrozenDict
|
||||
- anything else: assumed hashable
|
||||
"""
|
||||
if isinstance(item, FrozenDict):
|
||||
return item
|
||||
elif isinstance(item, list):
|
||||
return tuple(self._freeze(e) for e in item)
|
||||
elif isinstance(item, set):
|
||||
return frozenset(item)
|
||||
elif isinstance(item, dict):
|
||||
return FrozenDict(item)
|
||||
else:
|
||||
# any other type is assumed hashable
|
||||
return item
|
||||
|
||||
def __setitem__(self, key):
|
||||
raise RuntimeError("Cannot modify frozen {type(self).__name__}")
|
||||
|
||||
def update(self, other):
|
||||
raise RuntimeError("Cannot modify frozen {type(self).__name__}")
|
||||
|
||||
def __hash__(self):
|
||||
"""Cache hash because we are immutable"""
|
||||
if self._hash is None:
|
||||
self._hash = hash(tuple((key, value) for key, value in self.items()))
|
||||
return self._hash
|
@@ -3,8 +3,7 @@ import sys
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
@@ -13,6 +12,7 @@ config = context.config
|
||||
# This line sets up loggers basically.
|
||||
if 'jupyterhub' in sys.modules:
|
||||
from traitlets.config import MultipleInstanceError
|
||||
|
||||
from jupyterhub.app import JupyterHub
|
||||
|
||||
app = None
|
||||
|
@@ -11,8 +11,8 @@ down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
@@ -15,8 +15,8 @@ import logging
|
||||
|
||||
logger = logging.getLogger('alembic')
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
tables = ('oauth_access_tokens', 'oauth_codes')
|
||||
|
||||
|
@@ -22,8 +22,9 @@ import logging
|
||||
|
||||
logger = logging.getLogger('alembic')
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from jupyterhub.orm import JSONDict
|
||||
|
||||
|
||||
|
@@ -11,8 +11,9 @@ down_revision = '896818069c98'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from jupyterhub.orm import JSONDict
|
||||
|
||||
|
||||
|
@@ -11,11 +11,11 @@ down_revision = '1cebaf56856c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
import logging
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
logger = logging.getLogger('alembic')
|
||||
|
||||
|
||||
|
@@ -13,17 +13,12 @@ depends_on = None
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import Unicode
|
||||
from sqlalchemy import Column, ForeignKey, Integer, Table, Unicode
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from jupyterhub import orm
|
||||
from jupyterhub import roles
|
||||
from jupyterhub import orm, roles
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
@@ -12,12 +12,11 @@ down_revision = '4dc2d5a8c53c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from jupyterhub import orm
|
||||
|
||||
|
||||
naming_convention = orm.meta.naming_convention
|
||||
|
||||
|
||||
|
@@ -11,8 +11,8 @@ down_revision = 'd68c98b66cd4'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
@@ -12,11 +12,11 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('users', sa.Column('created', sa.DateTime, nullable=True))
|
||||
|
@@ -11,8 +11,8 @@ down_revision = 'eeb276e51423'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
@@ -11,8 +11,8 @@ down_revision = '99a28a4418e1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
@@ -12,8 +12,9 @@ down_revision = '19c0846f6344'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from jupyterhub.orm import JSONDict
|
||||
|
||||
|
||||
|
@@ -1,9 +1,4 @@
|
||||
from . import auth
|
||||
from . import groups
|
||||
from . import hub
|
||||
from . import proxy
|
||||
from . import services
|
||||
from . import users
|
||||
from . import auth, groups, hub, proxy, services, users
|
||||
from .base import *
|
||||
|
||||
default_handlers = []
|
||||
|
@@ -1,25 +1,16 @@
|
||||
"""Authorization handlers"""
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import itertools
|
||||
import json
|
||||
from datetime import datetime
|
||||
from urllib.parse import parse_qsl
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse
|
||||
|
||||
from oauthlib import oauth2
|
||||
from tornado import web
|
||||
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from .. import scopes
|
||||
from ..utils import get_browser_protocol
|
||||
from ..utils import token_authenticated
|
||||
from .base import APIHandler
|
||||
from .base import BaseHandler
|
||||
from .. import orm, roles, scopes
|
||||
from ..utils import get_browser_protocol, token_authenticated
|
||||
from .base import APIHandler, BaseHandler
|
||||
|
||||
|
||||
class TokenAPIHandler(APIHandler):
|
||||
@@ -38,7 +29,7 @@ class TokenAPIHandler(APIHandler):
|
||||
if owner:
|
||||
# having a token means we should be able to read the owner's model
|
||||
# (this is the only thing this handler is for)
|
||||
self.expanded_scopes.update(scopes.identify_scopes(owner))
|
||||
self.expanded_scopes |= scopes.identify_scopes(owner)
|
||||
self.parsed_scopes = scopes.parse_scopes(self.expanded_scopes)
|
||||
|
||||
# record activity whenever we see a token
|
||||
@@ -296,7 +287,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
|
||||
# rather than the expanded_scope intersection
|
||||
|
||||
required_scopes = {*scopes.identify_scopes(), *scopes.access_scopes(client)}
|
||||
user_scopes.update({"inherit", *required_scopes})
|
||||
user_scopes |= {"inherit", *required_scopes}
|
||||
|
||||
allowed_scopes = requested_scopes.intersection(user_scopes)
|
||||
excluded_scopes = requested_scopes.difference(user_scopes)
|
||||
|
@@ -4,10 +4,7 @@
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from http.client import responses
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from tornado import web
|
||||
@@ -15,9 +12,7 @@ from tornado import web
|
||||
from .. import orm
|
||||
from ..handlers import BaseHandler
|
||||
from ..scopes import get_scopes_for
|
||||
from ..utils import get_browser_protocol
|
||||
from ..utils import isoformat
|
||||
from ..utils import url_path_join
|
||||
from ..utils import get_browser_protocol, isoformat, url_path_join
|
||||
|
||||
PAGINATION_MEDIA_TYPE = "application/jupyterhub-pagination+json"
|
||||
|
||||
|
@@ -6,8 +6,7 @@ import json
|
||||
from tornado import web
|
||||
|
||||
from .. import orm
|
||||
from ..scopes import needs_scope
|
||||
from ..scopes import Scope
|
||||
from ..scopes import Scope, needs_scope
|
||||
from .base import APIHandler
|
||||
|
||||
|
||||
|
@@ -6,8 +6,7 @@ Currently GET-only, no actions can be taken to modify services.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import json
|
||||
|
||||
from ..scopes import needs_scope
|
||||
from ..scopes import Scope
|
||||
from ..scopes import Scope, needs_scope
|
||||
from .base import APIHandler
|
||||
|
||||
|
||||
|
@@ -3,26 +3,19 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from async_generator import aclosing
|
||||
from dateutil.parser import parse as parse_date
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import func, or_
|
||||
from tornado import web
|
||||
from tornado.iostream import StreamClosedError
|
||||
|
||||
from .. import orm
|
||||
from .. import scopes
|
||||
from .. import orm, scopes
|
||||
from ..roles import assign_default_roles
|
||||
from ..scopes import needs_scope
|
||||
from ..user import User
|
||||
from ..utils import isoformat
|
||||
from ..utils import iterate_until
|
||||
from ..utils import maybe_future
|
||||
from ..utils import url_path_join
|
||||
from ..utils import isoformat, iterate_until, maybe_future, url_path_join
|
||||
from .base import APIHandler
|
||||
|
||||
|
||||
@@ -51,7 +44,7 @@ class SelfAPIHandler(APIHandler):
|
||||
for scope in identify_scopes:
|
||||
if scope not in self.expanded_scopes:
|
||||
_added_scopes.add(scope)
|
||||
self.expanded_scopes.add(scope)
|
||||
self.expanded_scopes |= {scope}
|
||||
if _added_scopes:
|
||||
# re-parse with new scopes
|
||||
self.parsed_scopes = scopes.parse_scopes(self.expanded_scopes)
|
||||
|
@@ -14,15 +14,11 @@ import socket
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from getpass import getuser
|
||||
from operator import itemgetter
|
||||
from textwrap import dedent
|
||||
from urllib.parse import unquote
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
from urllib.parse import unquote, urlparse, urlunparse
|
||||
|
||||
if sys.version_info[:2] < (3, 3):
|
||||
raise ValueError("Python < 3.3 not supported: %s" % sys.version)
|
||||
@@ -36,78 +32,72 @@ except AttributeError as e:
|
||||
asyncio_all_tasks = asyncio.Task.all_tasks
|
||||
asyncio_current_task = asyncio.Task.current_task
|
||||
|
||||
from dateutil.parser import parse as parse_date
|
||||
from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader
|
||||
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
||||
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
import tornado.httpserver
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
from tornado.log import app_log, access_log, gen_log
|
||||
import tornado.options
|
||||
from dateutil.parser import parse as parse_date
|
||||
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
|
||||
from jupyter_telemetry.eventlog import EventLog
|
||||
from sqlalchemy.exc import OperationalError, SQLAlchemyError
|
||||
from tornado import gen, web
|
||||
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
from tornado.log import access_log, app_log, gen_log
|
||||
from traitlets import (
|
||||
Unicode,
|
||||
Integer,
|
||||
Dict,
|
||||
List,
|
||||
Bool,
|
||||
Any,
|
||||
Tuple,
|
||||
Set,
|
||||
Instance,
|
||||
Bool,
|
||||
Bytes,
|
||||
Dict,
|
||||
Float,
|
||||
Instance,
|
||||
Integer,
|
||||
List,
|
||||
Set,
|
||||
Tuple,
|
||||
Unicode,
|
||||
Union,
|
||||
observe,
|
||||
default,
|
||||
observe,
|
||||
validate,
|
||||
)
|
||||
from traitlets.config import Application, Configurable, catch_config_error
|
||||
|
||||
from jupyter_telemetry.eventlog import EventLog
|
||||
|
||||
here = os.path.dirname(__file__)
|
||||
|
||||
import jupyterhub
|
||||
from . import handlers, apihandlers
|
||||
from .handlers.static import CacheControlStaticFilesHandler, LogoHandler
|
||||
from .services.service import Service
|
||||
|
||||
from . import crypto
|
||||
from . import dbutil
|
||||
from . import orm
|
||||
from . import roles
|
||||
from . import scopes
|
||||
from .user import UserDict
|
||||
from .oauth.provider import make_provider
|
||||
from . import apihandlers, crypto, dbutil, handlers, orm, roles, scopes
|
||||
from ._data import DATA_FILES_PATH
|
||||
from .log import CoroutineLogFormatter, log_request
|
||||
from .proxy import Proxy, ConfigurableHTTPProxy
|
||||
from .traitlets import URLPrefix, Command, EntryPointType, Callable
|
||||
from .utils import (
|
||||
AnyTimeoutError,
|
||||
catch_db_error,
|
||||
maybe_future,
|
||||
url_path_join,
|
||||
print_stacks,
|
||||
print_ps_info,
|
||||
make_ssl_context,
|
||||
)
|
||||
from .metrics import HUB_STARTUP_DURATION_SECONDS
|
||||
from .metrics import INIT_SPAWNERS_DURATION_SECONDS
|
||||
from .metrics import RUNNING_SERVERS
|
||||
from .metrics import TOTAL_USERS
|
||||
|
||||
# classes for config
|
||||
from .auth import Authenticator, PAMAuthenticator
|
||||
from .crypto import CryptKeeper
|
||||
from .spawner import Spawner, LocalProcessSpawner
|
||||
from .objects import Hub, Server
|
||||
|
||||
# For faking stats
|
||||
from .emptyclass import EmptyClass
|
||||
from .handlers.static import CacheControlStaticFilesHandler, LogoHandler
|
||||
from .log import CoroutineLogFormatter, log_request
|
||||
from .metrics import (
|
||||
HUB_STARTUP_DURATION_SECONDS,
|
||||
INIT_SPAWNERS_DURATION_SECONDS,
|
||||
RUNNING_SERVERS,
|
||||
TOTAL_USERS,
|
||||
)
|
||||
from .oauth.provider import make_provider
|
||||
from .objects import Hub, Server
|
||||
from .proxy import ConfigurableHTTPProxy, Proxy
|
||||
from .services.service import Service
|
||||
from .spawner import LocalProcessSpawner, Spawner
|
||||
from .traitlets import Callable, Command, EntryPointType, URLPrefix
|
||||
from .user import UserDict
|
||||
from .utils import (
|
||||
AnyTimeoutError,
|
||||
catch_db_error,
|
||||
make_ssl_context,
|
||||
maybe_future,
|
||||
print_ps_info,
|
||||
print_stacks,
|
||||
url_path_join,
|
||||
)
|
||||
|
||||
common_aliases = {
|
||||
'log-level': 'Application.log_level',
|
||||
|
@@ -9,9 +9,7 @@ import warnings
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import partial
|
||||
from shutil import which
|
||||
from subprocess import PIPE
|
||||
from subprocess import Popen
|
||||
from subprocess import STDOUT
|
||||
from subprocess import PIPE, STDOUT, Popen
|
||||
|
||||
try:
|
||||
import pamela
|
||||
@@ -20,13 +18,12 @@ except Exception as e:
|
||||
_pamela_error = e
|
||||
|
||||
from tornado.concurrent import run_on_executor
|
||||
|
||||
from traitlets import Any, Bool, Dict, Integer, Set, Unicode, default, observe
|
||||
from traitlets.config import LoggingConfigurable
|
||||
from traitlets import Bool, Integer, Set, Unicode, Dict, Any, default, observe
|
||||
|
||||
from .handlers.login import LoginHandler
|
||||
from .utils import maybe_future, url_path_join
|
||||
from .traitlets import Command
|
||||
from .utils import maybe_future, url_path_join
|
||||
|
||||
|
||||
class Authenticator(LoggingConfigurable):
|
||||
|
@@ -4,18 +4,12 @@ import os
|
||||
from binascii import a2b_hex
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from traitlets import Any
|
||||
from traitlets import default
|
||||
from traitlets import Integer
|
||||
from traitlets import List
|
||||
from traitlets import observe
|
||||
from traitlets import validate
|
||||
from traitlets.config import Config
|
||||
from traitlets.config import SingletonConfigurable
|
||||
from traitlets import Any, Integer, List, default, observe, validate
|
||||
from traitlets.config import Config, SingletonConfigurable
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography.fernet import Fernet, MultiFernet, InvalidToken
|
||||
from cryptography.fernet import Fernet, InvalidToken, MultiFernet
|
||||
except ImportError:
|
||||
cryptography = None
|
||||
|
||||
|
@@ -1,7 +1,4 @@
|
||||
from . import base
|
||||
from . import login
|
||||
from . import metrics
|
||||
from . import pages
|
||||
from . import base, login, metrics, pages
|
||||
from .base import *
|
||||
from .login import *
|
||||
|
||||
|
@@ -9,48 +9,44 @@ import random
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from http.client import responses
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import parse_qsl
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse
|
||||
|
||||
from jinja2 import TemplateNotFound
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from tornado import gen
|
||||
from tornado import web
|
||||
from tornado.httputil import HTTPHeaders
|
||||
from tornado.httputil import url_concat
|
||||
from tornado import gen, web
|
||||
from tornado.httputil import HTTPHeaders, url_concat
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.log import app_log
|
||||
from tornado.web import addslash
|
||||
from tornado.web import RequestHandler
|
||||
from tornado.web import RequestHandler, addslash
|
||||
|
||||
from .. import __version__
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from .. import scopes
|
||||
from ..metrics import PROXY_ADD_DURATION_SECONDS
|
||||
from ..metrics import PROXY_DELETE_DURATION_SECONDS
|
||||
from ..metrics import ProxyDeleteStatus
|
||||
from ..metrics import RUNNING_SERVERS
|
||||
from ..metrics import SERVER_POLL_DURATION_SECONDS
|
||||
from ..metrics import SERVER_SPAWN_DURATION_SECONDS
|
||||
from ..metrics import SERVER_STOP_DURATION_SECONDS
|
||||
from ..metrics import ServerPollStatus
|
||||
from ..metrics import ServerSpawnStatus
|
||||
from ..metrics import ServerStopStatus
|
||||
from ..metrics import TOTAL_USERS
|
||||
from .. import __version__, orm, roles, scopes
|
||||
from ..metrics import (
|
||||
PROXY_ADD_DURATION_SECONDS,
|
||||
PROXY_DELETE_DURATION_SECONDS,
|
||||
RUNNING_SERVERS,
|
||||
SERVER_POLL_DURATION_SECONDS,
|
||||
SERVER_SPAWN_DURATION_SECONDS,
|
||||
SERVER_STOP_DURATION_SECONDS,
|
||||
TOTAL_USERS,
|
||||
ProxyDeleteStatus,
|
||||
ServerPollStatus,
|
||||
ServerSpawnStatus,
|
||||
ServerStopStatus,
|
||||
)
|
||||
from ..objects import Server
|
||||
from ..spawner import LocalProcessSpawner
|
||||
from ..user import User
|
||||
from ..utils import AnyTimeoutError
|
||||
from ..utils import get_accepted_mimetype
|
||||
from ..utils import maybe_future
|
||||
from ..utils import url_path_join
|
||||
|
||||
from ..utils import (
|
||||
AnyTimeoutError,
|
||||
get_accepted_mimetype,
|
||||
get_browser_protocol,
|
||||
maybe_future,
|
||||
url_path_join,
|
||||
)
|
||||
|
||||
|
||||
# pattern for the authentication token header
|
||||
auth_header_pat = re.compile(r'^(?:token|bearer)\s+([^\s]+)$', flags=re.IGNORECASE)
|
||||
@@ -515,7 +511,13 @@ class BaseHandler(RequestHandler):
|
||||
path=url_path_join(self.base_url, 'services'),
|
||||
**kwargs,
|
||||
)
|
||||
# clear tornado cookie
|
||||
# clear_cookie only accepts a subset of set_cookie's kwargs
|
||||
clear_xsrf_cookie_kwargs = {
|
||||
key: value
|
||||
for key, value in self.settings.get('xsrf_cookie_kwargs', {}).items()
|
||||
if key in {"path", "domain"}
|
||||
}
|
||||
|
||||
self.clear_cookie(
|
||||
'_xsrf',
|
||||
**self.settings.get('xsrf_cookie_kwargs', {}),
|
||||
|
@@ -1,7 +1,5 @@
|
||||
"""Handlers for serving prometheus metrics"""
|
||||
from prometheus_client import CONTENT_TYPE_LATEST
|
||||
from prometheus_client import generate_latest
|
||||
from prometheus_client import REGISTRY
|
||||
from prometheus_client import CONTENT_TYPE_LATEST, REGISTRY, generate_latest
|
||||
|
||||
from ..utils import metrics_authentication
|
||||
from .base import BaseHandler
|
||||
|
@@ -12,11 +12,9 @@ from tornado import web
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
from .. import __version__
|
||||
from ..metrics import SERVER_POLL_DURATION_SECONDS
|
||||
from ..metrics import ServerPollStatus
|
||||
from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus
|
||||
from ..scopes import needs_scope
|
||||
from ..utils import maybe_future
|
||||
from ..utils import url_path_join
|
||||
from ..utils import maybe_future, url_path_join
|
||||
from .base import BaseHandler
|
||||
|
||||
|
||||
|
@@ -6,13 +6,10 @@ import logging
|
||||
import traceback
|
||||
from functools import partial
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from tornado.log import access_log
|
||||
from tornado.log import LogFormatter
|
||||
from tornado.web import HTTPError
|
||||
from tornado.web import StaticFileHandler
|
||||
from tornado.log import LogFormatter, access_log
|
||||
from tornado.web import HTTPError, StaticFileHandler
|
||||
|
||||
from .handlers.pages import HealthCheckHandler
|
||||
from .metrics import prometheus_log_method
|
||||
|
@@ -21,8 +21,7 @@ them manually here.
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
from prometheus_client import Gauge
|
||||
from prometheus_client import Histogram
|
||||
from prometheus_client import Gauge, Histogram
|
||||
|
||||
REQUEST_DURATION_SECONDS = Histogram(
|
||||
'jupyterhub_request_duration_seconds',
|
||||
|
@@ -3,19 +3,14 @@
|
||||
implements https://oauthlib.readthedocs.io/en/latest/oauth2/server.html
|
||||
"""
|
||||
from oauthlib import uri_validate
|
||||
from oauthlib.oauth2 import RequestValidator
|
||||
from oauthlib.oauth2 import WebApplicationServer
|
||||
from oauthlib.oauth2.rfc6749.grant_types import authorization_code
|
||||
from oauthlib.oauth2.rfc6749.grant_types import base
|
||||
from oauthlib.oauth2 import RequestValidator, WebApplicationServer
|
||||
from oauthlib.oauth2.rfc6749.grant_types import authorization_code, base
|
||||
from tornado.log import app_log
|
||||
|
||||
from .. import orm
|
||||
from ..roles import roles_to_scopes
|
||||
from ..scopes import _check_scopes_exist
|
||||
from ..scopes import access_scopes
|
||||
from ..scopes import identify_scopes
|
||||
from ..utils import compare_token
|
||||
from ..utils import hash_token
|
||||
from ..scopes import _check_scopes_exist, access_scopes, identify_scopes
|
||||
from ..utils import compare_token, hash_token
|
||||
|
||||
# patch absolute-uri check
|
||||
# because we want to allow relative uri oauth
|
||||
@@ -159,9 +154,8 @@ class JupyterHubRequestValidator(RequestValidator):
|
||||
scopes = roles_to_scopes(orm_client.allowed_roles)
|
||||
if 'inherit' not in scopes:
|
||||
# add identify-user scope
|
||||
scopes.update(identify_scopes())
|
||||
# add access-service scope
|
||||
scopes.update(access_scopes(orm_client))
|
||||
# and access-service scope
|
||||
scopes |= identify_scopes() | access_scopes(orm_client)
|
||||
return scopes
|
||||
|
||||
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
|
||||
|
@@ -3,25 +3,20 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import socket
|
||||
import warnings
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from traitlets import default
|
||||
from traitlets import HasTraits
|
||||
from traitlets import Instance
|
||||
from traitlets import Integer
|
||||
from traitlets import observe
|
||||
from traitlets import Unicode
|
||||
from traitlets import validate
|
||||
from traitlets import HasTraits, Instance, Integer, Unicode, default, observe, validate
|
||||
|
||||
from . import orm
|
||||
from .traitlets import URLPrefix
|
||||
from .utils import can_connect
|
||||
from .utils import make_ssl_context
|
||||
from .utils import random_port
|
||||
from .utils import url_path_join
|
||||
from .utils import wait_for_http_server
|
||||
from .utils import wait_for_server
|
||||
from .utils import (
|
||||
can_connect,
|
||||
make_ssl_context,
|
||||
random_port,
|
||||
url_path_join,
|
||||
wait_for_http_server,
|
||||
wait_for_server,
|
||||
)
|
||||
|
||||
|
||||
class Server(HasTraits):
|
||||
|
@@ -3,47 +3,44 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import enum
|
||||
import json
|
||||
from base64 import decodebytes
|
||||
from base64 import encodebytes
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from base64 import decodebytes, encodebytes
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import alembic.command
|
||||
import alembic.config
|
||||
from alembic.script import ScriptDirectory
|
||||
from sqlalchemy import Boolean
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import Unicode
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
MetaData,
|
||||
Table,
|
||||
Unicode,
|
||||
create_engine,
|
||||
event,
|
||||
exc,
|
||||
inspect,
|
||||
or_,
|
||||
select,
|
||||
)
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import backref
|
||||
from sqlalchemy.orm import interfaces
|
||||
from sqlalchemy.orm import object_session
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import (
|
||||
Session,
|
||||
backref,
|
||||
interfaces,
|
||||
object_session,
|
||||
relationship,
|
||||
sessionmaker,
|
||||
)
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy.sql.expression import bindparam
|
||||
from sqlalchemy.types import LargeBinary
|
||||
from sqlalchemy.types import Text
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
from sqlalchemy.types import LargeBinary, Text, TypeDecorator
|
||||
from tornado.log import app_log
|
||||
|
||||
from .roles import roles_to_scopes
|
||||
from .utils import compare_token
|
||||
from .utils import hash_token
|
||||
from .utils import new_token
|
||||
from .utils import random_port
|
||||
from .utils import compare_token, hash_token, new_token, random_port
|
||||
|
||||
# top-level variable for easier mocking in tests
|
||||
utcnow = datetime.utcnow
|
||||
|
@@ -26,29 +26,18 @@ from subprocess import Popen
|
||||
from urllib.parse import quote
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httpclient import HTTPError
|
||||
from tornado.httpclient import HTTPRequest
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPError, HTTPRequest
|
||||
from tornado.ioloop import PeriodicCallback
|
||||
from traitlets import Any
|
||||
from traitlets import Bool
|
||||
from traitlets import default
|
||||
from traitlets import Dict
|
||||
from traitlets import Instance
|
||||
from traitlets import Integer
|
||||
from traitlets import observe
|
||||
from traitlets import Unicode
|
||||
from traitlets import Any, Bool, Dict, Instance, Integer, Unicode, default, observe
|
||||
from traitlets.config import LoggingConfigurable
|
||||
|
||||
from . import utils
|
||||
from .metrics import CHECK_ROUTES_DURATION_SECONDS
|
||||
from .metrics import PROXY_POLL_DURATION_SECONDS
|
||||
from .objects import Server
|
||||
from .utils import AnyTimeoutError
|
||||
from .utils import exponential_backoff
|
||||
from .utils import url_path_join
|
||||
from jupyterhub.traitlets import Command
|
||||
|
||||
from . import utils
|
||||
from .metrics import CHECK_ROUTES_DURATION_SECONDS, PROXY_POLL_DURATION_SECONDS
|
||||
from .objects import Server
|
||||
from .utils import AnyTimeoutError, exponential_backoff, url_path_join
|
||||
|
||||
|
||||
def _one_at_a_time(method):
|
||||
"""decorator to limit an async method to be called only once
|
||||
|
@@ -7,8 +7,7 @@ from functools import wraps
|
||||
from sqlalchemy import func
|
||||
from tornado.log import app_log
|
||||
|
||||
from . import orm
|
||||
from . import scopes
|
||||
from . import orm, scopes
|
||||
|
||||
|
||||
def get_default_roles():
|
||||
|
@@ -1,6 +1,10 @@
|
||||
"""
|
||||
General scope definitions and utilities
|
||||
|
||||
Scope functions generally return _immutable_ collections,
|
||||
such as `frozenset` to avoid mutating cached values.
|
||||
If needed, mutable copies can be made, e.g. `set(frozen_scopes)`
|
||||
|
||||
Scope variable nomenclature
|
||||
---------------------------
|
||||
scopes or 'raw' scopes: collection of scopes that may contain abbreviations (e.g., in role definition)
|
||||
@@ -23,8 +27,8 @@ import sqlalchemy as sa
|
||||
from tornado import web
|
||||
from tornado.log import app_log
|
||||
|
||||
from . import orm
|
||||
from . import roles
|
||||
from . import orm, roles
|
||||
from ._memoize import DoNotCache, FrozenDict, lru_cache_key
|
||||
|
||||
"""when modifying the scope definitions, make sure that `docs/source/rbac/generate-scope-table.py` is run
|
||||
so that changes are reflected in the documentation and REST API description."""
|
||||
@@ -145,6 +149,12 @@ class Scope(Enum):
|
||||
ALL = True
|
||||
|
||||
|
||||
def _intersection_cache_key(scopes_a, scopes_b, db=None):
|
||||
"""Cache key function for scope intersections"""
|
||||
return (frozenset(scopes_a), frozenset(scopes_b))
|
||||
|
||||
|
||||
@lru_cache_key(_intersection_cache_key)
|
||||
def _intersect_expanded_scopes(scopes_a, scopes_b, db=None):
|
||||
"""Intersect two sets of scopes by comparing their permissions
|
||||
|
||||
@@ -160,11 +170,16 @@ def _intersect_expanded_scopes(scopes_a, scopes_b, db=None):
|
||||
(i.e. users!group=x & users!user=y will be empty, even if user y is in group x.)
|
||||
"""
|
||||
empty_set = frozenset()
|
||||
scopes_a = frozenset(scopes_a)
|
||||
scopes_b = frozenset(scopes_b)
|
||||
|
||||
# cached lookups for group membership of users and servers
|
||||
@lru_cache()
|
||||
def groups_for_user(username):
|
||||
"""Get set of group names for a given username"""
|
||||
# if we need a group lookup, the result is not cacheable
|
||||
nonlocal needs_db
|
||||
needs_db = True
|
||||
user = db.query(orm.User).filter_by(name=username).first()
|
||||
if user is None:
|
||||
return empty_set
|
||||
@@ -180,6 +195,11 @@ def _intersect_expanded_scopes(scopes_a, scopes_b, db=None):
|
||||
parsed_scopes_a = parse_scopes(scopes_a)
|
||||
parsed_scopes_b = parse_scopes(scopes_b)
|
||||
|
||||
# track whether we need a db lookup (for groups)
|
||||
# because we can't cache the intersection if we do
|
||||
# if there are no group filters, this is cacheable
|
||||
needs_db = False
|
||||
|
||||
common_bases = parsed_scopes_a.keys() & parsed_scopes_b.keys()
|
||||
|
||||
common_filters = {}
|
||||
@@ -221,6 +241,7 @@ def _intersect_expanded_scopes(scopes_a, scopes_b, db=None):
|
||||
UserWarning,
|
||||
)
|
||||
warned = True
|
||||
needs_db = True
|
||||
|
||||
common_filters[base] = {
|
||||
entity: filters_a[entity] & filters_b[entity]
|
||||
@@ -246,6 +267,7 @@ def _intersect_expanded_scopes(scopes_a, scopes_b, db=None):
|
||||
# resolve group/server hierarchy if db available
|
||||
servers = servers.difference(common_servers)
|
||||
if db is not None and servers and 'group' in b:
|
||||
needs_db = True
|
||||
for server in servers:
|
||||
server_groups = groups_for_server(server)
|
||||
if server_groups & b['group']:
|
||||
@@ -273,7 +295,12 @@ def _intersect_expanded_scopes(scopes_a, scopes_b, db=None):
|
||||
if common_users and "user" not in common_filters[base]:
|
||||
common_filters[base]["user"] = common_users
|
||||
|
||||
return unparse_scopes(common_filters)
|
||||
intersection = unparse_scopes(common_filters)
|
||||
if needs_db:
|
||||
# return intersection, but don't cache it if it needed db lookups
|
||||
return DoNotCache(intersection)
|
||||
|
||||
return intersection
|
||||
|
||||
|
||||
def get_scopes_for(orm_object):
|
||||
@@ -314,7 +341,7 @@ def get_scopes_for(orm_object):
|
||||
# only thing we miss by short-circuiting here: warning about excluded extra scopes
|
||||
return owner_scopes
|
||||
|
||||
token_scopes = expand_scopes(token_scopes, owner=owner)
|
||||
token_scopes = set(expand_scopes(token_scopes, owner=owner))
|
||||
|
||||
if orm_object.client_id != "jupyterhub":
|
||||
# oauth tokens can be used to access the service issuing the token,
|
||||
@@ -359,6 +386,7 @@ def get_scopes_for(orm_object):
|
||||
return expanded_scopes
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def _expand_self_scope(username):
|
||||
"""
|
||||
Users have a metascope 'self' that should be expanded to standard user privileges.
|
||||
@@ -391,9 +419,11 @@ def _expand_self_scope(username):
|
||||
'read:tokens',
|
||||
'access:servers',
|
||||
]
|
||||
return {f"{scope}!user={username}" for scope in scope_list}
|
||||
# return immutable frozenset because the result is cached
|
||||
return frozenset(f"{scope}!user={username}" for scope in scope_list)
|
||||
|
||||
|
||||
@lru_cache(maxsize=65535)
|
||||
def _expand_scope(scope):
|
||||
"""Returns a scope and all all subscopes
|
||||
|
||||
@@ -434,9 +464,30 @@ def _expand_scope(scope):
|
||||
else:
|
||||
expanded_scopes = expanded_scope_names
|
||||
|
||||
return expanded_scopes
|
||||
# return immutable frozenset because the result is cached
|
||||
return frozenset(expanded_scopes)
|
||||
|
||||
|
||||
def _expand_scopes_key(scopes, owner=None):
|
||||
"""Cache key function for expand_scopes
|
||||
|
||||
scopes is usually a mutable list or set,
|
||||
which can be hashed as a frozenset
|
||||
|
||||
For the owner, we only care about what kind they are,
|
||||
and their name.
|
||||
"""
|
||||
# freeze scopes for hash
|
||||
frozen_scopes = frozenset(scopes)
|
||||
if owner is None:
|
||||
owner_key = None
|
||||
else:
|
||||
# owner key is the type and name
|
||||
owner_key = (type(owner).__name__, owner.name)
|
||||
return (frozen_scopes, owner_key)
|
||||
|
||||
|
||||
@lru_cache_key(_expand_scopes_key)
|
||||
def expand_scopes(scopes, owner=None):
|
||||
"""Returns a set of fully expanded scopes for a collection of raw scopes
|
||||
|
||||
@@ -480,8 +531,9 @@ def expand_scopes(scopes, owner=None):
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# reduce to minimize
|
||||
return reduce_scopes(expanded_scopes)
|
||||
# reduce to discard overlapping scopes
|
||||
# return immutable frozenset because the result is cached
|
||||
return frozenset(reduce_scopes(expanded_scopes))
|
||||
|
||||
|
||||
def _needs_scope_expansion(filter_, filter_value, sub_scope):
|
||||
@@ -615,6 +667,7 @@ def _check_token_scopes(scopes, owner):
|
||||
)
|
||||
|
||||
|
||||
@lru_cache_key(frozenset)
|
||||
def parse_scopes(scope_list):
|
||||
"""
|
||||
Parses scopes and filters in something akin to JSON style
|
||||
@@ -650,9 +703,11 @@ def parse_scopes(scope_list):
|
||||
parsed_scopes[base_scope][key] = {value}
|
||||
else:
|
||||
parsed_scopes[base_scope][key].add(value)
|
||||
return parsed_scopes
|
||||
# return immutable FrozenDict because the result is cached
|
||||
return FrozenDict(parsed_scopes)
|
||||
|
||||
|
||||
@lru_cache_key(FrozenDict)
|
||||
def unparse_scopes(parsed_scopes):
|
||||
"""Turn a parsed_scopes dictionary back into a expanded scopes set"""
|
||||
expanded_scopes = set()
|
||||
@@ -663,14 +718,17 @@ def unparse_scopes(parsed_scopes):
|
||||
for entity, names_list in filters.items():
|
||||
for name in names_list:
|
||||
expanded_scopes.add(f'{base}!{entity}={name}')
|
||||
return expanded_scopes
|
||||
# return immutable frozenset because the result is cached
|
||||
return frozenset(expanded_scopes)
|
||||
|
||||
|
||||
@lru_cache_key(frozenset)
|
||||
def reduce_scopes(expanded_scopes):
|
||||
"""Reduce expanded scopes to minimal set
|
||||
|
||||
Eliminates redundancy, such as access:services and access:services!service=x
|
||||
Eliminates overlapping scopes, such as access:services and access:services!service=x
|
||||
"""
|
||||
# unparse_scopes already returns a frozenset
|
||||
return unparse_scopes(parse_scopes(expanded_scopes))
|
||||
|
||||
|
||||
@@ -724,6 +782,14 @@ def needs_scope(*scopes):
|
||||
return scope_decorator
|
||||
|
||||
|
||||
def _identify_key(obj=None):
|
||||
if obj is None:
|
||||
return None
|
||||
else:
|
||||
return (type(obj).__name__, obj.name)
|
||||
|
||||
|
||||
@lru_cache_key(_identify_key)
|
||||
def identify_scopes(obj=None):
|
||||
"""Return 'identify' scopes for an orm object
|
||||
|
||||
@@ -736,20 +802,25 @@ def identify_scopes(obj=None):
|
||||
identify scopes (set): set of scopes needed for 'identify' endpoints
|
||||
"""
|
||||
if obj is None:
|
||||
return {f"read:users:{field}!user" for field in {"name", "groups"}}
|
||||
return frozenset(f"read:users:{field}!user" for field in {"name", "groups"})
|
||||
elif isinstance(obj, orm.User):
|
||||
return {f"read:users:{field}!user={obj.name}" for field in {"name", "groups"}}
|
||||
return frozenset(
|
||||
f"read:users:{field}!user={obj.name}" for field in {"name", "groups"}
|
||||
)
|
||||
elif isinstance(obj, orm.Service):
|
||||
return {f"read:services:{field}!service={obj.name}" for field in {"name"}}
|
||||
return frozenset(
|
||||
f"read:services:{field}!service={obj.name}" for field in {"name"}
|
||||
)
|
||||
else:
|
||||
raise TypeError(f"Expected orm.User or orm.Service, got {obj!r}")
|
||||
|
||||
|
||||
@lru_cache_key(lambda oauth_client: oauth_client.identifier)
|
||||
def access_scopes(oauth_client):
|
||||
"""Return scope(s) required to access an oauth client"""
|
||||
scopes = set()
|
||||
if oauth_client.identifier == "jupyterhub":
|
||||
return scopes
|
||||
return frozenset()
|
||||
spawner = oauth_client.spawner
|
||||
if spawner:
|
||||
scopes.add(f"access:servers!server={spawner.user.name}/{spawner.name}")
|
||||
@@ -761,9 +832,19 @@ def access_scopes(oauth_client):
|
||||
app_log.warning(
|
||||
f"OAuth client {oauth_client} has no associated service or spawner!"
|
||||
)
|
||||
return scopes
|
||||
return frozenset(scopes)
|
||||
|
||||
|
||||
def _check_scope_key(sub_scope, orm_resource, kind):
|
||||
"""Cache key function for check_scope_filter"""
|
||||
if kind == 'server':
|
||||
resource_key = (orm_resource.user.name, orm_resource.name)
|
||||
else:
|
||||
resource_key = orm_resource.name
|
||||
return (sub_scope, resource_key, kind)
|
||||
|
||||
|
||||
@lru_cache_key(_check_scope_key)
|
||||
def check_scope_filter(sub_scope, orm_resource, kind):
|
||||
"""Return whether a sub_scope filter applies to a given resource.
|
||||
|
||||
@@ -792,8 +873,8 @@ def check_scope_filter(sub_scope, orm_resource, kind):
|
||||
if kind == 'user' and 'group' in sub_scope:
|
||||
group_names = {group.name for group in orm_resource.groups}
|
||||
user_in_group = bool(group_names & set(sub_scope['group']))
|
||||
if user_in_group:
|
||||
return True
|
||||
# cannot cache if we needed to lookup groups in db
|
||||
return DoNotCache(user_in_group)
|
||||
return False
|
||||
|
||||
|
||||
@@ -832,6 +913,7 @@ def describe_parsed_scopes(parsed_scopes, username=None):
|
||||
return descriptions
|
||||
|
||||
|
||||
@lru_cache_key(lambda raw_scopes, username=None: (frozenset(raw_scopes), username))
|
||||
def describe_raw_scopes(raw_scopes, username=None):
|
||||
"""Return list of descriptions of raw scopes
|
||||
|
||||
@@ -862,7 +944,8 @@ def describe_raw_scopes(raw_scopes, username=None):
|
||||
"filter": filter_text,
|
||||
}
|
||||
)
|
||||
return descriptions
|
||||
# make sure we return immutable from a cached function
|
||||
return tuple(descriptions)
|
||||
|
||||
|
||||
# regex for custom scope
|
||||
|
@@ -40,21 +40,12 @@ from urllib.parse import urlencode
|
||||
import requests
|
||||
from tornado.httputil import url_concat
|
||||
from tornado.log import app_log
|
||||
from tornado.web import HTTPError
|
||||
from tornado.web import RequestHandler
|
||||
from traitlets import default
|
||||
from traitlets import Dict
|
||||
from traitlets import Instance
|
||||
from traitlets import Integer
|
||||
from traitlets import observe
|
||||
from traitlets import Set
|
||||
from traitlets import Unicode
|
||||
from traitlets import validate
|
||||
from tornado.web import HTTPError, RequestHandler
|
||||
from traitlets import Dict, Instance, Integer, Set, Unicode, default, observe, validate
|
||||
from traitlets.config import SingletonConfigurable
|
||||
|
||||
from ..scopes import _intersect_expanded_scopes
|
||||
from ..utils import get_browser_protocol
|
||||
from ..utils import url_path_join
|
||||
from ..utils import get_browser_protocol, url_path_join
|
||||
|
||||
|
||||
def check_scopes(required_scopes, scopes):
|
||||
|
@@ -45,21 +45,22 @@ import pipes
|
||||
import shutil
|
||||
from subprocess import Popen
|
||||
|
||||
from traitlets import Any
|
||||
from traitlets import Bool
|
||||
from traitlets import default
|
||||
from traitlets import Dict
|
||||
from traitlets import HasTraits
|
||||
from traitlets import Instance
|
||||
from traitlets import List
|
||||
from traitlets import Unicode
|
||||
from traitlets import validate
|
||||
from traitlets import (
|
||||
Any,
|
||||
Bool,
|
||||
Dict,
|
||||
HasTraits,
|
||||
Instance,
|
||||
List,
|
||||
Unicode,
|
||||
default,
|
||||
validate,
|
||||
)
|
||||
from traitlets.config import LoggingConfigurable
|
||||
|
||||
from .. import orm
|
||||
from ..objects import Server
|
||||
from ..spawner import LocalProcessSpawner
|
||||
from ..spawner import set_user_setuid
|
||||
from ..spawner import LocalProcessSpawner, set_user_setuid
|
||||
from ..traitlets import Command
|
||||
from ..utils import url_path_join
|
||||
|
||||
|
@@ -2,10 +2,8 @@
|
||||
|
||||
Contains default notebook-app subclass and mixins
|
||||
"""
|
||||
from .app import main
|
||||
from .app import SingleUserNotebookApp
|
||||
from .mixins import HubAuthenticatedHandler
|
||||
from .mixins import make_singleuser_app
|
||||
from .app import SingleUserNotebookApp, main
|
||||
from .mixins import HubAuthenticatedHandler, make_singleuser_app
|
||||
|
||||
# backward-compatibility
|
||||
JupyterHubLoginHandler = SingleUserNotebookApp.login_handler_class
|
||||
|
@@ -21,35 +21,29 @@ from datetime import timezone
|
||||
from textwrap import dedent
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from jinja2 import ChoiceLoader
|
||||
from jinja2 import FunctionLoader
|
||||
from jinja2 import ChoiceLoader, FunctionLoader
|
||||
from tornado import ioloop
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httpclient import HTTPRequest
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||
from tornado.web import RequestHandler
|
||||
from traitlets import Any
|
||||
from traitlets import Bool
|
||||
from traitlets import Bytes
|
||||
from traitlets import CUnicode
|
||||
from traitlets import default
|
||||
from traitlets import import_item
|
||||
from traitlets import Integer
|
||||
from traitlets import observe
|
||||
from traitlets import TraitError
|
||||
from traitlets import Unicode
|
||||
from traitlets import validate
|
||||
from traitlets import (
|
||||
Any,
|
||||
Bool,
|
||||
Bytes,
|
||||
CUnicode,
|
||||
Integer,
|
||||
TraitError,
|
||||
Unicode,
|
||||
default,
|
||||
import_item,
|
||||
observe,
|
||||
validate,
|
||||
)
|
||||
from traitlets.config import Configurable
|
||||
|
||||
from .._version import __version__
|
||||
from .._version import _check_version
|
||||
from .._version import __version__, _check_version
|
||||
from ..log import log_request
|
||||
from ..services.auth import HubOAuth
|
||||
from ..services.auth import HubOAuthCallbackHandler
|
||||
from ..services.auth import HubOAuthenticated
|
||||
from ..utils import exponential_backoff
|
||||
from ..utils import isoformat
|
||||
from ..utils import make_ssl_context
|
||||
from ..utils import url_path_join
|
||||
from ..services.auth import HubOAuth, HubOAuthCallbackHandler, HubOAuthenticated
|
||||
from ..utils import exponential_backoff, isoformat, make_ssl_context, url_path_join
|
||||
|
||||
|
||||
def _bool_env(key):
|
||||
|
@@ -19,29 +19,31 @@ from urllib.parse import urlparse
|
||||
from async_generator import aclosing
|
||||
from sqlalchemy import inspect
|
||||
from tornado.ioloop import PeriodicCallback
|
||||
from traitlets import Any
|
||||
from traitlets import Bool
|
||||
from traitlets import default
|
||||
from traitlets import Dict
|
||||
from traitlets import Float
|
||||
from traitlets import Instance
|
||||
from traitlets import Integer
|
||||
from traitlets import List
|
||||
from traitlets import observe
|
||||
from traitlets import Unicode
|
||||
from traitlets import Union
|
||||
from traitlets import validate
|
||||
from traitlets import (
|
||||
Any,
|
||||
Bool,
|
||||
Dict,
|
||||
Float,
|
||||
Instance,
|
||||
Integer,
|
||||
List,
|
||||
Unicode,
|
||||
Union,
|
||||
default,
|
||||
observe,
|
||||
validate,
|
||||
)
|
||||
from traitlets.config import LoggingConfigurable
|
||||
|
||||
from .objects import Server
|
||||
from .traitlets import ByteSpecification
|
||||
from .traitlets import Callable
|
||||
from .traitlets import Command
|
||||
from .utils import AnyTimeoutError
|
||||
from .utils import exponential_backoff
|
||||
from .utils import maybe_future
|
||||
from .utils import random_port
|
||||
from .utils import url_path_join
|
||||
from .traitlets import ByteSpecification, Callable, Command
|
||||
from .utils import (
|
||||
AnyTimeoutError,
|
||||
exponential_backoff,
|
||||
maybe_future,
|
||||
random_port,
|
||||
url_path_join,
|
||||
)
|
||||
|
||||
if os.name == 'nt':
|
||||
import psutil
|
||||
@@ -871,9 +873,6 @@ class Spawner(LoggingConfigurable):
|
||||
|
||||
if self.server:
|
||||
base_url = self.server.base_url
|
||||
if self.ip or self.port:
|
||||
self.server.ip = self.ip
|
||||
self.server.port = self.port
|
||||
env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
|
||||
else:
|
||||
# this should only occur in mock/testing scenarios
|
||||
|
@@ -35,22 +35,17 @@ from getpass import getuser
|
||||
from subprocess import TimeoutExpired
|
||||
from unittest import mock
|
||||
|
||||
from pytest import fixture
|
||||
from pytest import raises
|
||||
from pytest import fixture, raises
|
||||
from tornado import ioloop
|
||||
from tornado.httpclient import HTTPError
|
||||
from tornado.platform.asyncio import AsyncIOMainLoop
|
||||
|
||||
import jupyterhub.services.service
|
||||
from . import mocking
|
||||
from .. import crypto
|
||||
from .. import orm
|
||||
from .. import scopes
|
||||
from ..roles import create_role
|
||||
from ..roles import get_default_roles
|
||||
from ..roles import mock_roles
|
||||
from ..roles import update_roles
|
||||
|
||||
from .. import crypto, orm, scopes
|
||||
from ..roles import create_role, get_default_roles, mock_roles, update_roles
|
||||
from ..utils import random_port
|
||||
from . import mocking
|
||||
from .mocking import MockHub
|
||||
from .test_services import mockservice_cmd
|
||||
from .utils import add_user
|
||||
|
@@ -37,23 +37,15 @@ from urllib.parse import urlparse
|
||||
|
||||
from pamela import PAMError
|
||||
from tornado.ioloop import IOLoop
|
||||
from traitlets import Bool
|
||||
from traitlets import default
|
||||
from traitlets import Dict
|
||||
from traitlets import Bool, Dict, default
|
||||
|
||||
from .. import metrics
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from .. import metrics, orm, roles
|
||||
from ..app import JupyterHub
|
||||
from ..auth import PAMAuthenticator
|
||||
from ..singleuser import SingleUserNotebookApp
|
||||
from ..spawner import SimpleLocalProcessSpawner
|
||||
from ..utils import random_port
|
||||
from ..utils import utcnow
|
||||
from .utils import async_requests
|
||||
from .utils import public_host
|
||||
from .utils import public_url
|
||||
from .utils import ssl_setup
|
||||
from ..utils import random_port, utcnow
|
||||
from .utils import async_requests, public_host, public_url, ssl_setup
|
||||
|
||||
|
||||
def mock_authenticate(username, password, service, encoding):
|
||||
|
@@ -19,15 +19,14 @@ import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from tornado import httpserver
|
||||
from tornado import ioloop
|
||||
from tornado import log
|
||||
from tornado import web
|
||||
from tornado import httpserver, ioloop, log, web
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
from jupyterhub.services.auth import HubAuthenticated
|
||||
from jupyterhub.services.auth import HubOAuthCallbackHandler
|
||||
from jupyterhub.services.auth import HubOAuthenticated
|
||||
from jupyterhub.services.auth import (
|
||||
HubAuthenticated,
|
||||
HubOAuthCallbackHandler,
|
||||
HubOAuthenticated,
|
||||
)
|
||||
from jupyterhub.utils import make_ssl_context
|
||||
|
||||
|
||||
@@ -123,7 +122,7 @@ def main():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from tornado.options import parse_command_line, options
|
||||
from tornado.options import options, parse_command_line
|
||||
|
||||
parse_command_line()
|
||||
options.logging = 'debug'
|
||||
|
@@ -16,10 +16,7 @@ import os
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from tornado import httpserver
|
||||
from tornado import ioloop
|
||||
from tornado import log
|
||||
from tornado import web
|
||||
from tornado import httpserver, ioloop, log, web
|
||||
from tornado.options import options
|
||||
|
||||
from ..utils import make_ssl_context
|
||||
|
@@ -4,32 +4,23 @@ import json
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import mock
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
from urllib.parse import quote, urlparse, urlunparse
|
||||
|
||||
from pytest import fixture
|
||||
from pytest import mark
|
||||
from pytest import fixture, mark
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
import jupyterhub
|
||||
|
||||
from .. import orm
|
||||
from ..apihandlers.base import PAGINATION_MEDIA_TYPE
|
||||
from ..objects import Server
|
||||
from ..utils import url_path_join as ujoin
|
||||
from ..utils import utcnow
|
||||
from .conftest import new_username
|
||||
from .mocking import public_host
|
||||
from .mocking import public_url
|
||||
from .utils import add_user
|
||||
from .utils import api_request
|
||||
from .utils import async_requests
|
||||
from .utils import auth_header
|
||||
from .utils import find_user
|
||||
|
||||
from .mocking import public_host, public_url
|
||||
from .utils import add_user, api_request, async_requests, auth_header, find_user
|
||||
|
||||
# --------------------
|
||||
# Authentication tests
|
||||
|
@@ -6,11 +6,8 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from subprocess import check_output
|
||||
from subprocess import PIPE
|
||||
from subprocess import Popen
|
||||
from tempfile import NamedTemporaryFile
|
||||
from tempfile import TemporaryDirectory
|
||||
from subprocess import PIPE, Popen, check_output
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -19,8 +16,7 @@ from distutils.version import LooseVersion as V
|
||||
from traitlets.config import Config
|
||||
|
||||
from .. import orm
|
||||
from ..app import COOKIE_SECRET_BYTES
|
||||
from ..app import JupyterHub
|
||||
from ..app import COOKIE_SECRET_BYTES, JupyterHub
|
||||
from .mocking import MockHub
|
||||
from .test_api import add_user
|
||||
|
||||
|
@@ -10,16 +10,10 @@ from requests import HTTPError
|
||||
from traitlets import Any
|
||||
from traitlets.config import Config
|
||||
|
||||
from .mocking import MockPAMAuthenticator
|
||||
from .mocking import MockStructGroup
|
||||
from .mocking import MockStructPasswd
|
||||
from .utils import add_user
|
||||
from .utils import async_requests
|
||||
from .utils import get_page
|
||||
from .utils import public_url
|
||||
from jupyterhub import auth
|
||||
from jupyterhub import crypto
|
||||
from jupyterhub import orm
|
||||
from jupyterhub import auth, crypto, orm
|
||||
|
||||
from .mocking import MockPAMAuthenticator, MockStructGroup, MockStructPasswd
|
||||
from .utils import add_user, async_requests, get_page, public_url
|
||||
|
||||
|
||||
async def test_pam_auth():
|
||||
|
@@ -9,13 +9,11 @@ authentication can expire in a number of ways:
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
from unittest import mock
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
|
||||
from .utils import api_request
|
||||
from .utils import get_page
|
||||
from .utils import api_request, get_page
|
||||
|
||||
|
||||
async def refresh_expired(authenticator, user):
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import os
|
||||
from binascii import b2a_base64
|
||||
from binascii import b2a_hex
|
||||
from binascii import b2a_base64, b2a_hex
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from .. import crypto
|
||||
from ..crypto import decrypt
|
||||
from ..crypto import encrypt
|
||||
from ..crypto import decrypt, encrypt
|
||||
|
||||
keys = [('%i' % i).encode('ascii') * 32 for i in range(3)]
|
||||
hex_keys = [b2a_hex(key).decode('ascii') for key in keys]
|
||||
|
@@ -9,8 +9,7 @@ from pytest import raises
|
||||
from traitlets.config import Config
|
||||
|
||||
from .. import orm
|
||||
from ..app import NewToken
|
||||
from ..app import UpgradeDB
|
||||
from ..app import NewToken, UpgradeDB
|
||||
from ..scopes import _check_scopes_exist
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
@@ -16,7 +16,6 @@ from traitlets.config import Config
|
||||
|
||||
from .mocking import MockHub
|
||||
|
||||
|
||||
# To test new schemas, add them to the `valid_events`
|
||||
# and `invalid_events` dictionary below.
|
||||
|
||||
|
@@ -4,8 +4,7 @@ import time
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import ConnectionError
|
||||
from requests.exceptions import SSLError
|
||||
from requests.exceptions import ConnectionError, SSLError
|
||||
|
||||
from ..utils import AnyTimeoutError
|
||||
from .test_api import add_user
|
||||
|
94
jupyterhub/tests/test_memoize.py
Normal file
94
jupyterhub/tests/test_memoize.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import pytest
|
||||
|
||||
from jupyterhub._memoize import DoNotCache, FrozenDict, LRUCache, lru_cache_key
|
||||
|
||||
|
||||
def test_lru_cache():
|
||||
cache = LRUCache(maxsize=2)
|
||||
cache["a"] = 1
|
||||
assert "a" in cache
|
||||
assert "b" not in cache
|
||||
cache["b"] = 2
|
||||
assert cache["b"] == 2
|
||||
|
||||
# accessing a makes it more recent than b
|
||||
assert cache["a"] == 1
|
||||
assert "b" in cache
|
||||
assert "a" in cache
|
||||
|
||||
# storing c pushes oldest ('b') out of cache
|
||||
cache["c"] = 3
|
||||
assert len(cache._cache) == 2
|
||||
assert "a" in cache
|
||||
assert "c" in cache
|
||||
assert "b" not in cache
|
||||
|
||||
|
||||
def test_lru_cache_key():
|
||||
|
||||
call_count = 0
|
||||
|
||||
@lru_cache_key(frozenset)
|
||||
def reverse(arg):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return list(reversed(arg))
|
||||
|
||||
in1 = [1, 2]
|
||||
before = call_count
|
||||
out1 = reverse(in1)
|
||||
assert call_count == before + 1
|
||||
assert out1 == [2, 1]
|
||||
|
||||
before = call_count
|
||||
out2 = reverse(in1)
|
||||
assert call_count == before
|
||||
assert out2 is out1
|
||||
|
||||
|
||||
def test_do_not_cache():
|
||||
|
||||
call_count = 0
|
||||
|
||||
@lru_cache_key(lambda arg: arg)
|
||||
def is_even(arg):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if arg % 2:
|
||||
return DoNotCache(False)
|
||||
return True
|
||||
|
||||
before = call_count
|
||||
assert is_even(0) == True
|
||||
assert call_count == before + 1
|
||||
|
||||
# caches even results
|
||||
before = call_count
|
||||
assert is_even(0) == True
|
||||
assert call_count == before
|
||||
|
||||
before = call_count
|
||||
assert is_even(1) == False
|
||||
assert call_count == before + 1
|
||||
|
||||
# doesn't cache odd results
|
||||
before = call_count
|
||||
assert is_even(1) == False
|
||||
assert call_count == before + 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"d",
|
||||
[
|
||||
{"key": "value"},
|
||||
{"key": ["list"]},
|
||||
{"key": {"set"}},
|
||||
{"key": ("tu", "ple")},
|
||||
{"key": {"nested": ["dict"]}},
|
||||
],
|
||||
)
|
||||
def test_frozen_dict(d):
|
||||
frozen_1 = FrozenDict(d)
|
||||
frozen_2 = FrozenDict(d)
|
||||
assert hash(frozen_1) == hash(frozen_2)
|
||||
assert frozen_1 == frozen_2
|
@@ -3,11 +3,9 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from .utils import api_request
|
||||
from .utils import get_page
|
||||
from jupyterhub import metrics
|
||||
from jupyterhub import orm
|
||||
from jupyterhub import roles
|
||||
from jupyterhub import metrics, orm, roles
|
||||
|
||||
from .utils import api_request, get_page
|
||||
|
||||
|
||||
async def test_total_users(app):
|
||||
|
@@ -2,22 +2,15 @@
|
||||
import asyncio
|
||||
import json
|
||||
from unittest import mock
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
import pytest
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
from ..utils import url_path_join
|
||||
from .mocking import FormSpawner
|
||||
from .mocking import public_url
|
||||
from .test_api import add_user
|
||||
from .test_api import api_request
|
||||
from .test_api import fill_user
|
||||
from .test_api import normalize_user
|
||||
from .test_api import TIMESTAMP
|
||||
from .utils import async_requests
|
||||
from .utils import get_page
|
||||
from .mocking import FormSpawner, public_url
|
||||
from .test_api import TIMESTAMP, add_user, api_request, fill_user, normalize_user
|
||||
from .utils import async_requests, get_page
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@@ -3,17 +3,13 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import os
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from tornado import gen
|
||||
|
||||
from .. import crypto
|
||||
from .. import objects
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from .. import crypto, objects, orm, roles
|
||||
from ..emptyclass import EmptyClass
|
||||
from ..user import User
|
||||
from .mocking import MockSpawner
|
||||
|
@@ -2,31 +2,28 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from unittest import mock
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
from tornado.escape import url_escape
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from .. import scopes
|
||||
from .. import orm, roles, scopes
|
||||
from ..auth import Authenticator
|
||||
from ..handlers import BaseHandler
|
||||
from ..utils import url_path_join
|
||||
from ..utils import url_path_join as ujoin
|
||||
from .mocking import FalsyCallableFormSpawner
|
||||
from .mocking import FormSpawner
|
||||
from .mocking import FalsyCallableFormSpawner, FormSpawner
|
||||
from .test_api import next_event
|
||||
from .utils import api_request
|
||||
from .utils import async_requests
|
||||
from .utils import AsyncSession
|
||||
from .utils import get_page
|
||||
from .utils import public_host
|
||||
from .utils import public_url
|
||||
from .utils import (
|
||||
AsyncSession,
|
||||
api_request,
|
||||
async_requests,
|
||||
get_page,
|
||||
public_host,
|
||||
public_url,
|
||||
)
|
||||
|
||||
|
||||
async def test_root_no_auth(app):
|
||||
|
@@ -3,8 +3,7 @@ import json
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from subprocess import Popen
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
import pytest
|
||||
from traitlets.config import Config
|
||||
@@ -12,8 +11,7 @@ from traitlets.config import Config
|
||||
from ..utils import url_path_join as ujoin
|
||||
from ..utils import wait_for_http_server
|
||||
from .mocking import MockHub
|
||||
from .test_api import add_user
|
||||
from .test_api import api_request
|
||||
from .test_api import add_user, api_request
|
||||
from .utils import skip_if_ssl
|
||||
|
||||
|
||||
|
@@ -8,14 +8,11 @@ import pytest
|
||||
from pytest import mark
|
||||
from tornado.log import app_log
|
||||
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from ..scopes import get_scopes_for
|
||||
from ..scopes import scope_definitions
|
||||
from .. import orm, roles
|
||||
from ..scopes import get_scopes_for, scope_definitions
|
||||
from ..utils import utcnow
|
||||
from .mocking import MockHub
|
||||
from .utils import add_user
|
||||
from .utils import api_request
|
||||
from .utils import add_user, api_request
|
||||
|
||||
|
||||
@mark.role
|
||||
|
@@ -7,22 +7,21 @@ from pytest import mark
|
||||
from tornado import web
|
||||
from tornado.httputil import HTTPServerRequest
|
||||
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from .. import scopes
|
||||
from .. import orm, roles, scopes
|
||||
from .._memoize import FrozenDict
|
||||
from ..handlers import BaseHandler
|
||||
from ..scopes import _check_scope_access
|
||||
from ..scopes import _expand_self_scope
|
||||
from ..scopes import _intersect_expanded_scopes
|
||||
from ..scopes import expand_scopes
|
||||
from ..scopes import get_scopes_for
|
||||
from ..scopes import identify_scopes
|
||||
from ..scopes import needs_scope
|
||||
from ..scopes import parse_scopes
|
||||
from ..scopes import Scope
|
||||
from .utils import add_user
|
||||
from .utils import api_request
|
||||
from .utils import auth_header
|
||||
from ..scopes import (
|
||||
Scope,
|
||||
_check_scope_access,
|
||||
_expand_self_scope,
|
||||
_intersect_expanded_scopes,
|
||||
expand_scopes,
|
||||
get_scopes_for,
|
||||
identify_scopes,
|
||||
needs_scope,
|
||||
parse_scopes,
|
||||
)
|
||||
from .utils import add_user, api_request, auth_header
|
||||
|
||||
|
||||
def get_handler_with_scopes(scopes):
|
||||
@@ -40,6 +39,7 @@ def test_scope_constructor():
|
||||
f'read:users!user={user2}',
|
||||
]
|
||||
parsed_scopes = parse_scopes(scope_list)
|
||||
assert isinstance(parsed_scopes, FrozenDict)
|
||||
|
||||
assert 'read:users' in parsed_scopes
|
||||
assert parsed_scopes['users']
|
||||
@@ -469,6 +469,7 @@ async def test_metascope_self_expansion(
|
||||
orm_obj = create_service_with_scopes('self')
|
||||
# test expansion of user/service scopes
|
||||
scopes = get_scopes_for(orm_obj)
|
||||
assert isinstance(scopes, frozenset)
|
||||
assert bool(scopes) == has_user_scopes
|
||||
|
||||
# test expansion of token scopes
|
||||
@@ -490,6 +491,8 @@ async def test_metascope_inherit_expansion(app, create_user_with_scopes):
|
||||
token.scopes.clear()
|
||||
app.db.commit()
|
||||
token_scope_set = get_scopes_for(token)
|
||||
assert isinstance(token_scope_set, frozenset)
|
||||
|
||||
assert token_scope_set.issubset(identify_scopes(user.orm_user))
|
||||
|
||||
|
||||
@@ -1172,4 +1175,5 @@ def test_expand_scopes(user, scopes, expected):
|
||||
expected.update(_expand_self_scope(user.name))
|
||||
|
||||
expanded = expand_scopes(scopes, owner=user.orm_user)
|
||||
assert isinstance(expanded, frozenset)
|
||||
assert sorted(expanded) == sorted(expected)
|
||||
|
@@ -8,14 +8,15 @@ from async_generator import asynccontextmanager
|
||||
|
||||
from .. import orm
|
||||
from ..roles import update_roles
|
||||
from ..utils import exponential_backoff
|
||||
from ..utils import maybe_future
|
||||
from ..utils import random_port
|
||||
from ..utils import url_path_join
|
||||
from ..utils import wait_for_http_server
|
||||
from ..utils import (
|
||||
exponential_backoff,
|
||||
maybe_future,
|
||||
random_port,
|
||||
url_path_join,
|
||||
wait_for_http_server,
|
||||
)
|
||||
from .mocking import public_url
|
||||
from .utils import async_requests
|
||||
from .utils import skip_if_ssl
|
||||
from .utils import async_requests, skip_if_ssl
|
||||
|
||||
mockservice_path = os.path.dirname(os.path.abspath(__file__))
|
||||
mockservice_py = os.path.join(mockservice_path, 'mockservice.py')
|
||||
|
@@ -4,9 +4,7 @@ import os
|
||||
import sys
|
||||
from binascii import hexlify
|
||||
from unittest import mock
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import parse_qs, quote, urlparse
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
@@ -14,14 +12,11 @@ from pytest import raises
|
||||
from tornado.httputil import url_concat
|
||||
from tornado.log import app_log
|
||||
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from .. import scopes
|
||||
from .. import orm, roles, scopes
|
||||
from ..services.auth import _ExpiringDict
|
||||
from ..utils import url_path_join
|
||||
from .mocking import public_url
|
||||
from .utils import async_requests
|
||||
from .utils import AsyncSession
|
||||
from .utils import AsyncSession, async_requests
|
||||
|
||||
# mock for sending monotonic counter way into the future
|
||||
monotonic_future = mock.patch('time.monotonic', lambda: sys.maxsize)
|
||||
@@ -365,7 +360,7 @@ async def test_oauth_service_roles(
|
||||
)
|
||||
|
||||
if 'inherit' in expected_scopes:
|
||||
expected_scopes = scopes.get_scopes_for(user.orm_user)
|
||||
expected_scopes = set(scopes.get_scopes_for(user.orm_user))
|
||||
|
||||
# always expect identify/access scopes
|
||||
# on successful authentication
|
||||
|
@@ -2,20 +2,18 @@
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from subprocess import CalledProcessError
|
||||
from subprocess import check_output
|
||||
from subprocess import CalledProcessError, check_output
|
||||
from unittest import mock
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
|
||||
import jupyterhub
|
||||
|
||||
from .. import orm
|
||||
from ..utils import url_path_join
|
||||
from .mocking import public_url
|
||||
from .mocking import StubSingleUserSpawner
|
||||
from .utils import async_requests
|
||||
from .utils import AsyncSession
|
||||
from .mocking import StubSingleUserSpawner, public_url
|
||||
from .utils import AsyncSession, async_requests
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
@@ -16,14 +16,10 @@ import pytest
|
||||
|
||||
from .. import orm
|
||||
from .. import spawner as spawnermod
|
||||
from ..objects import Hub
|
||||
from ..objects import Server
|
||||
from ..spawner import LocalProcessSpawner
|
||||
from ..spawner import Spawner
|
||||
from ..objects import Hub, Server
|
||||
from ..spawner import LocalProcessSpawner, Spawner
|
||||
from ..user import User
|
||||
from ..utils import AnyTimeoutError
|
||||
from ..utils import new_token
|
||||
from ..utils import url_path_join
|
||||
from ..utils import AnyTimeoutError, new_token, url_path_join
|
||||
from .mocking import public_url
|
||||
from .test_api import add_user
|
||||
from .utils import async_requests
|
||||
|
@@ -1,10 +1,7 @@
|
||||
import pytest
|
||||
from traitlets import HasTraits
|
||||
from traitlets import TraitError
|
||||
from traitlets import HasTraits, TraitError
|
||||
|
||||
from jupyterhub.traitlets import ByteSpecification
|
||||
from jupyterhub.traitlets import Command
|
||||
from jupyterhub.traitlets import URLPrefix
|
||||
from jupyterhub.traitlets import ByteSpecification, Command, URLPrefix
|
||||
|
||||
|
||||
def test_url_prefix():
|
||||
|
@@ -3,8 +3,7 @@ import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from .._version import _check_version
|
||||
from .._version import reset_globals
|
||||
from .._version import _check_version, reset_globals
|
||||
|
||||
|
||||
def setup_function(function):
|
||||
|
@@ -7,11 +7,9 @@ import pytest
|
||||
import requests
|
||||
from certipy import Certipy
|
||||
|
||||
from jupyterhub import metrics
|
||||
from jupyterhub import orm
|
||||
from jupyterhub import metrics, orm
|
||||
from jupyterhub.objects import Server
|
||||
from jupyterhub.roles import assign_default_roles
|
||||
from jupyterhub.roles import update_roles
|
||||
from jupyterhub.roles import assign_default_roles, update_roles
|
||||
from jupyterhub.utils import url_path_join as ujoin
|
||||
|
||||
|
||||
|
@@ -4,13 +4,7 @@ Traitlets that are used in JupyterHub
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import entrypoints
|
||||
from traitlets import Integer
|
||||
from traitlets import List
|
||||
from traitlets import TraitError
|
||||
from traitlets import TraitType
|
||||
from traitlets import Type
|
||||
from traitlets import Undefined
|
||||
from traitlets import Unicode
|
||||
from traitlets import Integer, List, TraitError, TraitType, Type, Undefined, Unicode
|
||||
|
||||
|
||||
class URLPrefix(Unicode):
|
||||
|
@@ -3,34 +3,21 @@
|
||||
import json
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
from sqlalchemy import inspect
|
||||
from tornado import gen
|
||||
from tornado import web
|
||||
from tornado import gen, web
|
||||
from tornado.httputil import urlencode
|
||||
from tornado.log import app_log
|
||||
|
||||
from . import orm
|
||||
from ._version import __version__
|
||||
from ._version import _check_version
|
||||
from .crypto import CryptKeeper
|
||||
from .crypto import decrypt
|
||||
from .crypto import encrypt
|
||||
from .crypto import EncryptionUnavailable
|
||||
from .crypto import InvalidToken
|
||||
from .metrics import RUNNING_SERVERS
|
||||
from .metrics import TOTAL_USERS
|
||||
from ._version import __version__, _check_version
|
||||
from .crypto import CryptKeeper, EncryptionUnavailable, InvalidToken, decrypt, encrypt
|
||||
from .metrics import RUNNING_SERVERS, TOTAL_USERS
|
||||
from .objects import Server
|
||||
from .spawner import LocalProcessSpawner
|
||||
from .utils import AnyTimeoutError
|
||||
from .utils import make_ssl_context
|
||||
from .utils import maybe_future
|
||||
from .utils import url_path_join
|
||||
|
||||
from .utils import AnyTimeoutError, make_ssl_context, maybe_future, url_path_join
|
||||
|
||||
# detailed messages about the most common failure-to-start errors,
|
||||
# which manifest timeouts during start
|
||||
|
@@ -16,18 +16,14 @@ import threading
|
||||
import uuid
|
||||
import warnings
|
||||
from binascii import b2a_hex
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from datetime import datetime, timezone
|
||||
from hmac import compare_digest
|
||||
from operator import itemgetter
|
||||
|
||||
from async_generator import aclosing
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from tornado import gen
|
||||
from tornado import ioloop
|
||||
from tornado import web
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httpclient import HTTPError
|
||||
from tornado import gen, ioloop, web
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPError
|
||||
from tornado.log import app_log
|
||||
|
||||
# For compatibility with python versions 3.6 or earlier.
|
||||
@@ -477,6 +473,7 @@ def print_stacks(file=sys.stderr):
|
||||
# no need to add them to startup
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from .log import coroutine_frames
|
||||
|
||||
print("Active threads: %i" % threading.active_count(), file=file)
|
||||
|
@@ -1,3 +1,6 @@
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.black]
|
||||
skip-string-normalization = true
|
||||
target_version = [
|
||||
|
4
setup.py
4
setup.py
@@ -9,13 +9,11 @@ import shutil
|
||||
import sys
|
||||
from subprocess import check_call
|
||||
|
||||
from setuptools import Command
|
||||
from setuptools import setup
|
||||
from setuptools import Command, 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
|
||||
if v[:2] < (3, 6):
|
||||
error = "ERROR: JupyterHub requires Python version 3.6 or above."
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user