Handle empty scopes and always send the WWW-Authenticate header, as per spec
Fixes #1045
This commit is contained in:
parent
c8f43ed08e
commit
ca7d36bf14
10 changed files with 47 additions and 41 deletions
|
@ -3,13 +3,13 @@ import re
|
||||||
|
|
||||||
from jsonschema import validate, ValidationError
|
from jsonschema import validate, ValidationError
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import request
|
from flask import request, url_for
|
||||||
from flask.ext.principal import identity_changed, Identity
|
from flask.ext.principal import identity_changed, Identity
|
||||||
from cryptography.x509 import load_pem_x509_certificate
|
from cryptography.x509 import load_pem_x509_certificate
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cachetools import lru_cache
|
from cachetools import lru_cache
|
||||||
|
|
||||||
from app import app
|
from app import app, get_app_url
|
||||||
from .auth_context import set_grant_context, get_grant_context
|
from .auth_context import set_grant_context, get_grant_context
|
||||||
from .permissions import repository_read_grant, repository_write_grant
|
from .permissions import repository_read_grant, repository_write_grant
|
||||||
from util.names import parse_namespace_repository
|
from util.names import parse_namespace_repository
|
||||||
|
@ -152,6 +152,18 @@ def build_context_and_subject(user, token, oauthtoken):
|
||||||
return (context, ANONYMOUS_SUB)
|
return (context, ANONYMOUS_SUB)
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_headers():
|
||||||
|
""" Returns a dictionary of headers for auth responses. """
|
||||||
|
headers = {}
|
||||||
|
realm_auth_path = url_for('v2.generate_registry_jwt')
|
||||||
|
authenticate = 'Bearer realm="{0}{1}",service="{2}"'.format(get_app_url(),
|
||||||
|
realm_auth_path,
|
||||||
|
app.config['SERVER_HOSTNAME'])
|
||||||
|
headers['WWW-Authenticate'] = authenticate
|
||||||
|
headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
|
def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
|
||||||
""" Process a bearer token and return the loaded identity, or raise InvalidJWTException if an
|
""" Process a bearer token and return the loaded identity, or raise InvalidJWTException if an
|
||||||
identity could not be loaded. Expects tokens and grants in the format of the Docker registry
|
identity could not be loaded. Expects tokens and grants in the format of the Docker registry
|
||||||
|
@ -219,7 +231,7 @@ def load_public_key(certificate_file_path):
|
||||||
return cert_obj.public_key()
|
return cert_obj.public_key()
|
||||||
|
|
||||||
|
|
||||||
def process_jwt_auth(func):
|
def process_registry_jwt_auth(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
logger.debug('Called with params: %s, %s', args, kwargs)
|
logger.debug('Called with params: %s, %s', args, kwargs)
|
||||||
|
@ -237,8 +249,7 @@ def process_jwt_auth(func):
|
||||||
set_grant_context(context)
|
set_grant_context(context)
|
||||||
logger.debug('Identity changed to %s', extracted_identity.id)
|
logger.debug('Identity changed to %s', extracted_identity.id)
|
||||||
except InvalidJWTException as ije:
|
except InvalidJWTException as ije:
|
||||||
abort(401, message=ije.message)
|
abort(401, message=ije.message, headers=get_auth_headers())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug('No auth header.')
|
logger.debug('No auth header.')
|
||||||
|
|
|
@ -4,7 +4,7 @@ import random
|
||||||
from app import analytics, app, userevents
|
from app import analytics, app, userevents
|
||||||
from data import model
|
from data import model
|
||||||
from flask import request
|
from flask import request
|
||||||
from auth.jwt_auth import get_granted_entity
|
from auth.registry_jwt_auth import get_granted_entity
|
||||||
from auth.auth_context import (get_authenticated_user, get_validated_token,
|
from auth.auth_context import (get_authenticated_user, get_validated_token,
|
||||||
get_validated_oauth_token)
|
get_validated_oauth_token)
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from time import time
|
||||||
from app import storage as store, image_replication_queue, app
|
from app import storage as store, image_replication_queue, app
|
||||||
from auth.auth import process_auth, extract_namespace_repo_from_session
|
from auth.auth import process_auth, extract_namespace_repo_from_session
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth.jwt_auth import get_granted_username
|
from auth.registry_jwt_auth import get_granted_username
|
||||||
from digest import checksums
|
from digest import checksums
|
||||||
from util.registry import changes
|
from util.registry import changes
|
||||||
from util.http import abort, exact_abort
|
from util.http import abort, exact_abort
|
||||||
|
|
|
@ -9,16 +9,13 @@ import features
|
||||||
from app import metric_queue
|
from app import metric_queue
|
||||||
from endpoints.decorators import anon_protect, anon_allowed
|
from endpoints.decorators import anon_protect, anon_allowed
|
||||||
from endpoints.v2.errors import V2RegistryException
|
from endpoints.v2.errors import V2RegistryException
|
||||||
from auth.jwt_auth import process_jwt_auth
|
|
||||||
from auth.auth_context import get_grant_context
|
from auth.auth_context import get_grant_context
|
||||||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
||||||
AdministerRepositoryPermission)
|
AdministerRepositoryPermission)
|
||||||
from data import model
|
from data import model
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from util.saas.metricqueue import time_blueprint
|
from util.saas.metricqueue import time_blueprint
|
||||||
from util import get_app_url
|
from auth.registry_jwt_auth import process_registry_jwt_auth, get_auth_headers
|
||||||
from app import app
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
v2_bp = Blueprint('v2', __name__)
|
v2_bp = Blueprint('v2', __name__)
|
||||||
|
@ -75,21 +72,15 @@ def route_show_if(value):
|
||||||
|
|
||||||
@v2_bp.route('/')
|
@v2_bp.route('/')
|
||||||
@route_show_if(features.ADVERTISE_V2)
|
@route_show_if(features.ADVERTISE_V2)
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
def v2_support_enabled():
|
def v2_support_enabled():
|
||||||
response = make_response('true', 200)
|
response = make_response('true', 200)
|
||||||
|
|
||||||
if get_grant_context() is None:
|
if get_grant_context() is None:
|
||||||
response = make_response('true', 401)
|
response = make_response('true', 401)
|
||||||
realm_auth_path = url_for('v2.generate_registry_jwt')
|
|
||||||
|
|
||||||
authenticate = 'Bearer realm="{0}{1}",service="{2}"'.format(get_app_url(app.config),
|
response.headers.extend(get_auth_headers())
|
||||||
realm_auth_path,
|
|
||||||
app.config['SERVER_HOSTNAME'])
|
|
||||||
response.headers['WWW-Authenticate'] = authenticate
|
|
||||||
|
|
||||||
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ import re
|
||||||
from flask import make_response, url_for, request, redirect, Response, abort as flask_abort
|
from flask import make_response, url_for, request, redirect, Response, abort as flask_abort
|
||||||
|
|
||||||
from app import storage, app
|
from app import storage, app
|
||||||
|
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||||
from data import model, database
|
from data import model, database
|
||||||
from digest import digest_tools
|
from digest import digest_tools
|
||||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream
|
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream
|
||||||
from endpoints.v2.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported,
|
from endpoints.v2.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported,
|
||||||
NameUnknown)
|
NameUnknown)
|
||||||
from auth.jwt_auth import process_jwt_auth
|
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from util.cache import cache_control
|
from util.cache import cache_control
|
||||||
from util.registry.filelike import wrap_with_handler, StreamSlice
|
from util.registry.filelike import wrap_with_handler, StreamSlice
|
||||||
|
@ -53,7 +53,7 @@ def _base_blob_fetch(namespace, repo_name, digest):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@cache_control(max_age=31436000)
|
@cache_control(max_age=31436000)
|
||||||
|
@ -68,7 +68,7 @@ def check_blob_exists(namespace, repo_name, digest):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@cache_control(max_age=31536000)
|
@cache_control(max_age=31536000)
|
||||||
|
@ -101,7 +101,7 @@ def _render_range(num_uploaded_bytes, with_bytes_prefix=True):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/', methods=['POST'])
|
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/', methods=['POST'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def start_blob_upload(namespace, repo_name):
|
def start_blob_upload(namespace, repo_name):
|
||||||
|
@ -134,7 +134,7 @@ def start_blob_upload(namespace, repo_name):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def fetch_existing_upload(namespace, repo_name, upload_uuid):
|
def fetch_existing_upload(namespace, repo_name, upload_uuid):
|
||||||
|
@ -290,7 +290,7 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def upload_chunk(namespace, repo_name, upload_uuid):
|
def upload_chunk(namespace, repo_name, upload_uuid):
|
||||||
|
@ -308,7 +308,7 @@ def upload_chunk(namespace, repo_name, upload_uuid):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
||||||
|
@ -326,7 +326,7 @@ def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def cancel_upload(namespace, repo_name, upload_uuid):
|
def cancel_upload(namespace, repo_name, upload_uuid):
|
||||||
|
@ -345,7 +345,7 @@ def cancel_upload(namespace, repo_name, upload_uuid):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/<digest>', methods=['DELETE'])
|
@v2_bp.route('/<namespace>/<repo_name>/blobs/<digest>', methods=['DELETE'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def delete_digest(namespace, repo_name, upload_uuid):
|
def delete_digest(namespace, repo_name, upload_uuid):
|
||||||
|
|
|
@ -9,7 +9,7 @@ from jwkest.jws import SIGNER_ALGS, keyrep
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from app import docker_v2_signing_key
|
from app import docker_v2_signing_key
|
||||||
from auth.jwt_auth import process_jwt_auth
|
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
|
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
|
||||||
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnverified,
|
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnverified,
|
||||||
|
@ -212,7 +212,7 @@ class SignedManifestBuilder(object):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
|
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||||
|
@ -241,7 +241,7 @@ def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET'])
|
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||||
|
@ -261,7 +261,7 @@ def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
|
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||||
|
@ -277,7 +277,7 @@ def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT'])
|
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||||
|
@ -375,7 +375,7 @@ def _write_manifest(namespace, repo_name, manifest):
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
|
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
|
def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
from flask import jsonify, url_for
|
from flask import jsonify, url_for
|
||||||
|
|
||||||
|
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||||
from endpoints.v2 import v2_bp, require_repo_read
|
from endpoints.v2 import v2_bp, require_repo_read
|
||||||
from endpoints.v2.errors import NameUnknown
|
from endpoints.v2.errors import NameUnknown
|
||||||
from endpoints.v2.v2util import add_pagination
|
from endpoints.v2.v2util import add_pagination
|
||||||
from auth.jwt_auth import process_jwt_auth
|
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from data import model
|
from data import model
|
||||||
|
|
||||||
@v2_bp.route('/<namespace>/<repo_name>/tags/list', methods=['GET'])
|
@v2_bp.route('/<namespace>/<repo_name>/tags/list', methods=['GET'])
|
||||||
@process_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def list_all_tags(namespace, repo_name):
|
def list_all_tags(namespace, repo_name):
|
||||||
|
|
|
@ -9,7 +9,7 @@ from cachetools import lru_cache
|
||||||
from app import app
|
from app import app
|
||||||
from data import model
|
from data import model
|
||||||
from auth.auth import process_auth
|
from auth.auth import process_auth
|
||||||
from auth.jwt_auth import build_context_and_subject
|
from auth.registry_jwt_auth import build_context_and_subject
|
||||||
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
||||||
from auth.permissions import (ModifyRepositoryPermission, ReadRepositoryPermission,
|
from auth.permissions import (ModifyRepositoryPermission, ReadRepositoryPermission,
|
||||||
CreateRepositoryPermission)
|
CreateRepositoryPermission)
|
||||||
|
@ -49,7 +49,7 @@ def generate_registry_jwt():
|
||||||
audience_param = request.args.get('service')
|
audience_param = request.args.get('service')
|
||||||
logger.debug('Request audience: %s', audience_param)
|
logger.debug('Request audience: %s', audience_param)
|
||||||
|
|
||||||
scope_param = request.args.get('scope')
|
scope_param = request.args.get('scope') or ''
|
||||||
logger.debug('Scope request: %s', scope_param)
|
logger.debug('Scope request: %s', scope_param)
|
||||||
|
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
|
@ -62,7 +62,8 @@ def generate_registry_jwt():
|
||||||
logger.debug('Authenticated OAuth token: %s', oauthtoken)
|
logger.debug('Authenticated OAuth token: %s', oauthtoken)
|
||||||
|
|
||||||
access = []
|
access = []
|
||||||
if scope_param is not None:
|
|
||||||
|
if len(scope_param) > 0:
|
||||||
match = SCOPE_REGEX.match(scope_param)
|
match = SCOPE_REGEX.match(scope_param)
|
||||||
if match is None:
|
if match is None:
|
||||||
logger.debug('Match: %s', match)
|
logger.debug('Match: %s', match)
|
||||||
|
|
|
@ -1193,6 +1193,9 @@ class V1LoginTests(V1RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, Base
|
||||||
class V2LoginTests(V2RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, BaseRegistryMixin, LiveServerTestCase):
|
class V2LoginTests(V2RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, BaseRegistryMixin, LiveServerTestCase):
|
||||||
""" Tests for V2 login. """
|
""" Tests for V2 login. """
|
||||||
|
|
||||||
|
def test_nouser_noscope(self):
|
||||||
|
self.do_login('', '', expected_code=401, scope='')
|
||||||
|
|
||||||
def test_validuser_unknownrepo(self):
|
def test_validuser_unknownrepo(self):
|
||||||
self.do_login('devtable', 'password', expect_success=False,
|
self.do_login('devtable', 'password', expect_success=False,
|
||||||
scope='repository:invalidnamespace/simple:pull')
|
scope='repository:invalidnamespace/simple:pull')
|
||||||
|
|
|
@ -7,8 +7,8 @@ from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from endpoints.v2.v2auth import TOKEN_VALIDITY_LIFETIME_S, load_certificate_bytes, load_private_key
|
from endpoints.v2.v2auth import TOKEN_VALIDITY_LIFETIME_S, load_certificate_bytes, load_private_key
|
||||||
from auth.jwt_auth import (identity_from_bearer_token, load_public_key, InvalidJWTException,
|
from auth.registry_jwt_auth import (identity_from_bearer_token, load_public_key,
|
||||||
build_context_and_subject, ANONYMOUS_SUB)
|
InvalidJWTException, build_context_and_subject, ANONYMOUS_SUB)
|
||||||
from util.morecollections import AttrDict
|
from util.morecollections import AttrDict
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in a new issue