mirror of
https://github.com/quay/quay.git
synced 2025-11-20 21:42:27 +03:00
* Convert all Python2 to Python3 syntax. * Removes oauth2lib dependency * Replace mockredis with fakeredis * byte/str conversions * Removes nonexisting __nonzero__ in Python3 * Python3 Dockerfile and related * [PROJQUAY-98] Replace resumablehashlib with rehash * PROJQUAY-123 - replace gpgme with python3-gpg * [PROJQUAY-135] Fix unhashable class error * Update external dependencies for Python 3 - Move github.com/app-registry/appr to github.com/quay/appr - github.com/coderanger/supervisor-stdout - github.com/DevTable/container-cloud-config - Update to latest mockldap with changes applied from coreos/mockldap - Update dependencies in requirements.txt and requirements-dev.txt * Default FLOAT_REPR function to str in json encoder and removes keyword assignment True, False, and str were not keywords in Python2... * [PROJQUAY-165] Replace package `bencode` with `bencode.py` - Bencode is not compatible with Python 3.x and is no longer maintained. Bencode.py appears to be a drop-in replacement/fork that is compatible with Python 3. * Make sure monkey.patch is called before anything else ( * Removes anunidecode dependency and replaces it with text_unidecode * Base64 encode/decode pickle dumps/loads when storing value in DB Base64 encodes/decodes the serialized values when storing them in the DB. Also make sure to return a Python3 string instead of a Bytes when coercing for db, otherwise, Postgres' TEXT field will convert it into a hex representation when storing the value. * Implement __hash__ on Digest class In Python 3, if a class defines __eq__() but not __hash__(), its instances will not be usable as items in hashable collections (e.g sets). * Remove basestring check * Fix expected message in credentials tests * Fix usage of Cryptography.Fernet for Python3 (#219) - Specifically, this addresses the issue where Byte<->String conversions weren't being applied correctly. * Fix utils - tar+stream layer format utils - filelike util * Fix storage tests * Fix endpoint tests * Fix workers tests * Fix docker's empty layer bytes * Fix registry tests * Appr * Enable CI for Python 3.6 * Skip buildman tests Skip buildman tests while it's being rewritten to allow ci to pass. * Install swig for CI * Update expected exception type in redis validation test * Fix gpg signing calls Fix gpg calls for updated gpg wrapper, and add signing tests. * Convert / to // for Python3 integer division * WIP: Update buildman to use asyncio instead of trollius. This dependency is considered deprecated/abandoned and was only used as an implementation/backport of asyncio on Python 2.x This is a work in progress, and is included in the PR just to get the rest of the tests passing. The builder is actually being rewritten. * Target Python 3.8 * Removes unused files - Removes unused files that were added accidentally while rebasing - Small fixes/cleanup - TODO tasks comments * Add TODO to verify rehash backward compat with resumablehashlib * Revert "[PROJQUAY-135] Fix unhashable class error" and implements __hash__ instead. This reverts commit 735e38e3c1d072bf50ea864bc7e119a55d3a8976. Instead, defines __hash__ for encryped fields class, using the parent field's implementation. * Remove some unused files ad imports Co-authored-by: Kenny Lee Sin Cheong <kenny.lee@redhat.com> Co-authored-by: Tom McKay <thomasmckay@redhat.com>
254 lines
7.5 KiB
Python
254 lines
7.5 KiB
Python
import copy
|
|
import logging
|
|
import urllib.request, urllib.parse, urllib.error
|
|
import urllib.parse
|
|
|
|
from abc import ABCMeta, abstractmethod
|
|
from six import add_metaclass
|
|
|
|
from util.config import URLSchemeAndHostname
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OAuthEndpoint(object):
|
|
def __init__(self, base_url, params=None):
|
|
self.base_url = base_url
|
|
self.params = params or {}
|
|
|
|
def with_param(self, name, value):
|
|
params_copy = copy.copy(self.params)
|
|
params_copy[name] = value
|
|
return OAuthEndpoint(self.base_url, params_copy)
|
|
|
|
def with_params(self, parameters):
|
|
params_copy = copy.copy(self.params)
|
|
params_copy.update(parameters)
|
|
return OAuthEndpoint(self.base_url, params_copy)
|
|
|
|
def to_url(self):
|
|
(scheme, netloc, path, _, fragment) = urllib.parse.urlsplit(self.base_url)
|
|
updated_query = urllib.parse.urlencode(self.params)
|
|
return urllib.parse.urlunsplit((scheme, netloc, path, updated_query, fragment))
|
|
|
|
|
|
class OAuthExchangeCodeException(Exception):
|
|
"""
|
|
Exception raised if a code exchange fails.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class OAuthGetUserInfoException(Exception):
|
|
"""
|
|
Exception raised if a call to get user information fails.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
@add_metaclass(ABCMeta)
|
|
class OAuthService(object):
|
|
"""
|
|
A base class for defining an external service, exposed via OAuth.
|
|
"""
|
|
|
|
def __init__(self, config, key_name):
|
|
self.key_name = key_name
|
|
self.config = config.get(key_name) or {}
|
|
|
|
@abstractmethod
|
|
def service_id(self):
|
|
"""
|
|
The internal ID for this service.
|
|
|
|
Must match the URL portion for the service, e.g. `github`
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def service_name(self):
|
|
"""
|
|
The user-readable name for the service, e.g. `GitHub`
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def token_endpoint(self):
|
|
"""
|
|
Returns the endpoint at which the OAuth code can be exchanged for a token.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def user_endpoint(self):
|
|
"""
|
|
Returns the endpoint at which user information can be looked up.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def authorize_endpoint(self):
|
|
"""
|
|
Returns the for authorization of the OAuth service.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def validate_client_id_and_secret(self, http_client, url_scheme_and_hostname):
|
|
"""
|
|
Performs validation of the client ID and secret, raising an exception on failure.
|
|
"""
|
|
pass
|
|
|
|
def requires_form_encoding(self):
|
|
"""
|
|
Returns True if form encoding is necessary for the exchange_code_for_token call.
|
|
"""
|
|
return False
|
|
|
|
def client_id(self):
|
|
return self.config.get("CLIENT_ID")
|
|
|
|
def client_secret(self):
|
|
return self.config.get("CLIENT_SECRET")
|
|
|
|
def login_binding_field(self):
|
|
"""
|
|
Returns the name of the field (`username` or `email`) used for auto binding an external
|
|
login service account to an *internal* login service account.
|
|
|
|
For example, if the external login service is GitHub and the internal login service is LDAP,
|
|
a value of `email` here will cause login-with-Github to conduct a search (via email) in LDAP
|
|
for a user, an auto bind the external and internal users together. May return None, in which
|
|
case no binding is performing, and login with this external account will simply create a new
|
|
account in the database.
|
|
"""
|
|
return self.config.get("LOGIN_BINDING_FIELD", None)
|
|
|
|
def get_auth_url(self, url_scheme_and_hostname, redirect_suffix, csrf_token, scopes):
|
|
"""
|
|
Retrieves the authorization URL for this login service.
|
|
"""
|
|
redirect_uri = "%s/oauth2/%s/callback%s" % (
|
|
url_scheme_and_hostname.get_url(),
|
|
self.service_id(),
|
|
redirect_suffix,
|
|
)
|
|
params = {
|
|
"client_id": self.client_id(),
|
|
"redirect_uri": redirect_uri,
|
|
"scope": " ".join(scopes),
|
|
"state": csrf_token,
|
|
}
|
|
|
|
return self.authorize_endpoint().with_params(params).to_url()
|
|
|
|
def get_redirect_uri(self, url_scheme_and_hostname, redirect_suffix=""):
|
|
return "%s://%s/oauth2/%s/callback%s" % (
|
|
url_scheme_and_hostname.url_scheme,
|
|
url_scheme_and_hostname.hostname,
|
|
self.service_id(),
|
|
redirect_suffix,
|
|
)
|
|
|
|
def get_user_info(self, http_client, token):
|
|
token_param = {
|
|
"alt": "json",
|
|
}
|
|
|
|
headers = {
|
|
"Authorization": "Bearer %s" % token,
|
|
}
|
|
|
|
got_user = http_client.get(
|
|
self.user_endpoint().to_url(), params=token_param, headers=headers
|
|
)
|
|
if got_user.status_code // 100 != 2:
|
|
raise OAuthGetUserInfoException(
|
|
"Non-2XX response code for user_info call: %s" % got_user.status_code
|
|
)
|
|
|
|
user_info = got_user.json()
|
|
if user_info is None:
|
|
raise OAuthGetUserInfoException()
|
|
|
|
return user_info
|
|
|
|
def exchange_code_for_token(
|
|
self,
|
|
app_config,
|
|
http_client,
|
|
code,
|
|
form_encode=False,
|
|
redirect_suffix="",
|
|
client_auth=False,
|
|
):
|
|
"""
|
|
Exchanges an OAuth access code for the associated OAuth token.
|
|
"""
|
|
json_data = self.exchange_code(
|
|
app_config, http_client, code, form_encode, redirect_suffix, client_auth
|
|
)
|
|
|
|
access_token = json_data.get("access_token", None)
|
|
if access_token is None:
|
|
logger.debug(
|
|
"Got successful get_access_token response with missing token: %s", json_data
|
|
)
|
|
raise OAuthExchangeCodeException("Missing `access_token` in OAuth response")
|
|
|
|
return access_token
|
|
|
|
def exchange_code(
|
|
self,
|
|
app_config,
|
|
http_client,
|
|
code,
|
|
form_encode=False,
|
|
redirect_suffix="",
|
|
client_auth=False,
|
|
):
|
|
"""
|
|
Exchanges an OAuth access code for associated OAuth token and other data.
|
|
"""
|
|
url_scheme_and_hostname = URLSchemeAndHostname.from_app_config(app_config)
|
|
payload = {
|
|
"code": code,
|
|
"grant_type": "authorization_code",
|
|
"redirect_uri": self.get_redirect_uri(url_scheme_and_hostname, redirect_suffix),
|
|
}
|
|
|
|
headers = {"Accept": "application/json"}
|
|
|
|
auth = None
|
|
if client_auth:
|
|
auth = (self.client_id(), self.client_secret())
|
|
else:
|
|
payload["client_id"] = self.client_id()
|
|
payload["client_secret"] = self.client_secret()
|
|
|
|
token_url = self.token_endpoint().to_url()
|
|
if form_encode:
|
|
get_access_token = http_client.post(token_url, data=payload, headers=headers, auth=auth)
|
|
else:
|
|
get_access_token = http_client.post(
|
|
token_url, params=payload, headers=headers, auth=auth
|
|
)
|
|
|
|
if get_access_token.status_code // 100 != 2:
|
|
logger.debug("Got get_access_token response %s", get_access_token.text)
|
|
raise OAuthExchangeCodeException(
|
|
"Got non-2XX response for code exchange: %s" % get_access_token.status_code
|
|
)
|
|
|
|
json_data = get_access_token.json()
|
|
if not json_data:
|
|
raise OAuthExchangeCodeException("Got non-JSON response for code exchange")
|
|
|
|
if "error" in json_data:
|
|
raise OAuthExchangeCodeException(json_data.get("error_description", json_data["error"]))
|
|
|
|
return json_data
|