Skip to content

Commit

Permalink
Merge pull request #5322 from hypothesis/remove-auth-client-logic
Browse files Browse the repository at this point in the history
Remove (legacy) auth client logic
  • Loading branch information
robertknight committed Oct 1, 2018
2 parents a0a6cca + 51e09d0 commit 334e5f8
Show file tree
Hide file tree
Showing 3 changed files with 0 additions and 248 deletions.
87 changes: 0 additions & 87 deletions h/auth/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
import base64
import re

import hmac
Expand All @@ -11,45 +10,7 @@

from h.auth import role
from h._compat import text_type
from h.exceptions import ClientUnauthorized
from h.models.auth_client import GrantType, AuthClient
from h.schemas import ValidationError


def basic_auth_creds(request):
"""
Extract any HTTP Basic authentication credentials for the request.
Returns a tuple with the HTTP Basic access authentication credentials
``(username, password)`` if provided, otherwise ``None``.
:param request: the request object
:type request: pyramid.request.Request
:returns: a tuple of (username, password) or None
:rtype: tuple or NoneType
"""
try:
authtype, value = request.authorization
except TypeError: # no authorization header
return None
if authtype.lower() != 'basic':
return None
try:
user_pass_bytes = base64.standard_b64decode(value)
except TypeError: # failed to decode
return None
try:
# See the lengthy comment in the tests about why we assume UTF-8
# encoding here.
user_pass = user_pass_bytes.decode('utf-8')
except UnicodeError: # not UTF-8
return None
try:
username, password = user_pass.split(':', 1)
except ValueError: # not enough values to unpack
return None
return (username, password)


def groupfinder(userid, request):
Expand Down Expand Up @@ -205,51 +166,3 @@ def principals_for_auth_client_user(user, client):
distinct_principals = list(set(all_principals))

return distinct_principals


def request_auth_client(request):
"""
Locate a matching AuthClient record in the database.
:param request: the request object
:type request: pyramid.request.Request
:returns: an auth client
:rtype: an AuthClient model
:raises ClientUnauthorized: if the client does not have a valid Client ID
and Client Secret or is not allowed to create users in their authority.
"""
creds = basic_auth_creds(request)
if creds is None:
raise ClientUnauthorized()

# We fetch the client by its ID and then do a constant-time comparison of
# the secret with that provided in the request.
#
# It is important not to include the secret as part of the SQL query
# because the resulting code may be subject to a timing attack.
client_id, client_secret = creds
try:
client = request.db.query(AuthClient).get(client_id)
except sa.exc.StatementError: # client_id is malformed
raise ClientUnauthorized()
if client is None:
raise ClientUnauthorized()
if client.secret is None: # client is not confidential
raise ClientUnauthorized()
if client.grant_type != GrantType.client_credentials: # client not allowed to create users
raise ClientUnauthorized()

if not hmac.compare_digest(client.secret, client_secret):
raise ClientUnauthorized()

return client


def validate_auth_client_authority(client, authority):
"""
Validate that the auth client authority matches the provided ``authority``.
"""
if client.authority != authority:
raise ValidationError("'authority' does not match authenticated client")
156 changes: 0 additions & 156 deletions tests/h/auth/util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,106 +2,24 @@

from __future__ import unicode_literals

import base64
from collections import namedtuple
from h._compat import unichr

import pytest
import mock
from hypothesis import strategies as st
from hypothesis import given
import sqlalchemy as sa

from pyramid import security

from h.auth import role
from h.auth import util
from h._compat import text_type
from h.exceptions import ClientUnauthorized
from h.models.auth_client import GrantType
from h.models import AuthClient
from h.schemas import ValidationError
from h.services.user import UserService

FakeUser = namedtuple('FakeUser', ['authority', 'admin', 'staff', 'groups'])
FakeGroup = namedtuple('FakeGroup', ['pubid'])

# The most recent standard covering the 'Basic' HTTP Authentication scheme is
# RFC 7617. It defines the allowable characters in usernames and passwords as
# follows:
#
# The user-id and password MUST NOT contain any control characters (see
# "CTL" in Appendix B.1 of [RFC5234]).
#
# RFC5234 defines CTL as:
#
# CTL = %x00-1F / %x7F
#
CONTROL_CHARS = set(unichr(n) for n in range(0x00, 0x1F + 1)) | set('\x7f')

# We assume user ID and password strings are UTF-8 and surrogates are not
# allowed in UTF-8.
SURROGATE_CHARS = set(unichr(n) for n in range(0xD800, 0xDBFF + 1)) | \
set(unichr(n) for n in range(0xDC00, 0xDFFF + 1))
INVALID_USER_PASS_CHARS = CONTROL_CHARS | SURROGATE_CHARS

# Furthermore, from RFC 7617:
#
# a user-id containing a colon character is invalid
#
INVALID_USERNAME_CHARS = INVALID_USER_PASS_CHARS | set(':')

# The character encoding of the user-id and password is *undefined* by
# specification for historical reasons:
#
# The original definition of this authentication scheme failed to specify
# the character encoding scheme used to convert the user-pass into an
# octet sequence. In practice, most implementations chose either a
# locale-specific encoding such as ISO-8859-1 ([ISO-8859-1]), or UTF-8
# ([RFC3629]). For backwards compatibility reasons, this specification
# continues to leave the default encoding undefined, as long as it is
# compatible with US-ASCII (mapping any US-ASCII character to a single
# octet matching the US-ASCII character code).
#
# In particular, Firefox still does *very* special things if you provide
# non-BMP characters in a username or password.
#
# There's not a lot we can do about this so we are going to assume UTF-8
# encoding for the user-pass string, and these tests verify that we
# successfully decode valid Unicode user-pass strings.
#
VALID_USERNAME_CHARS = st.characters(blacklist_characters=INVALID_USERNAME_CHARS)
VALID_PASSWORD_CHARS = st.characters(blacklist_characters=INVALID_USER_PASS_CHARS)


class TestBasicAuthCreds(object):

@given(username=st.text(alphabet=VALID_USERNAME_CHARS),
password=st.text(alphabet=VALID_PASSWORD_CHARS))
def test_valid(self, username, password, pyramid_request):
user_pass = username + ':' + password
creds = ('Basic', base64.standard_b64encode(user_pass.encode('utf-8')))
pyramid_request.authorization = creds

assert util.basic_auth_creds(pyramid_request) == (username, password)

def test_missing(self, pyramid_request):
pyramid_request.authorization = None

assert util.basic_auth_creds(pyramid_request) is None

def test_no_password(self, pyramid_request):
creds = ('Basic', base64.standard_b64encode('foobar'.encode('utf-8')))
pyramid_request.authorization = creds

assert util.basic_auth_creds(pyramid_request) is None

def test_other_authorization_type(self, pyramid_request):
creds = ('Digest', base64.standard_b64encode('foo:bar'.encode('utf-8')))
pyramid_request.authorization = creds

assert util.basic_auth_creds(pyramid_request) is None


class TestGroupfinder(object):
def test_it_fetches_the_user(self, pyramid_request, user_service):
Expand Down Expand Up @@ -223,21 +141,6 @@ def test_it_returns_text_type(self, pyramid_request):
assert type(util.default_authority(pyramid_request)) == text_type


class TestValidateAuthClientAuthority(object):

def test_raises_when_authority_doesnt_match(self, pyramid_request, auth_client):
authority = 'mismatched_authority'

with pytest.raises(ValidationError,
match=".*authority.*does not match authenticated client"):
util.validate_auth_client_authority(auth_client, authority)

def test_does_not_raise_when_authority_matches(self, pyramid_request, auth_client):
authority = 'weylandindustries.com'

util.validate_auth_client_authority(auth_client, authority)


class TestPrincipalsForAuthClient(object):

def test_it_sets_auth_client_principal(self, auth_client):
Expand Down Expand Up @@ -370,65 +273,6 @@ def hmac(self, patch):
return patch('h.auth.util.hmac')


class TestRequestAuthClient(object):

def test_raises_when_no_creds(self, pyramid_request, basic_auth_creds):
with pytest.raises(ClientUnauthorized):
util.request_auth_client(pyramid_request)

def test_raises_when_malformed_client_id(self,
basic_auth_creds,
pyramid_request):
basic_auth_creds.return_value = ('foobar', 'somerandomsecret')

with pytest.raises(ClientUnauthorized):
util.request_auth_client(pyramid_request)

def test_raises_when_no_client(self,
basic_auth_creds,
pyramid_request):
basic_auth_creds.return_value = ('C69BA868-5089-4EE4-ABB6-63A1C38C395B',
'somerandomsecret')

with pytest.raises(ClientUnauthorized):
util.request_auth_client(pyramid_request)

def test_raises_when_client_secret_invalid(self,
auth_client,
basic_auth_creds,
pyramid_request):
basic_auth_creds.return_value = (auth_client.id, 'incorrectsecret')

with pytest.raises(ClientUnauthorized):
util.request_auth_client(pyramid_request)

def test_raises_for_public_client(self,
factories,
basic_auth_creds,
pyramid_request):
auth_client = factories.AuthClient(authority='weylandindustries.com')
basic_auth_creds.return_value = (auth_client.id, '')

with pytest.raises(ClientUnauthorized):
util.request_auth_client(pyramid_request)

def test_raises_for_invalid_client_grant_type(self,
factories,
basic_auth_creds,
pyramid_request):
auth_client = factories.ConfidentialAuthClient(authority='weylandindustries.com',
grant_type=GrantType.authorization_code)
basic_auth_creds.return_value = (auth_client.id, auth_client.secret)

with pytest.raises(ClientUnauthorized):
util.request_auth_client(pyramid_request)

def test_returns_client_when_valid_creds(self, pyramid_request, auth_client, valid_auth):
client = util.request_auth_client(pyramid_request)

assert client == auth_client


@pytest.fixture
def user_service(pyramid_config):
service = mock.create_autospec(UserService, spec_set=True, instance=True)
Expand Down
5 changes: 0 additions & 5 deletions tests/h/views/api/users_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,6 @@ def auth_client(factories):
grant_type=GrantType.client_credentials)


@pytest.fixture
def validate_auth_client_authority(patch):
return patch('h.views.api.users.validate_auth_client_authority')


@pytest.fixture
def user_signup_service(db_session, pyramid_config, user):
service = mock.Mock(spec_set=UserSignupService(default_authority='example.com',
Expand Down

0 comments on commit 334e5f8

Please sign in to comment.