From eab33de97e99e4360a6ba09e3a420c7ce5efd4c9 Mon Sep 17 00:00:00 2001 From: allegroai <> Date: Mon, 3 May 2021 17:52:25 +0300 Subject: [PATCH] Add bcrypt support to fixed user password --- apiserver/config/default/apiserver.conf | 2 + apiserver/requirements.txt | 1 + apiserver/service_repo/auth/auth.py | 62 +++++++++++++++++------ apiserver/service_repo/auth/fixed_user.py | 4 ++ 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/apiserver/config/default/apiserver.conf b/apiserver/config/default/apiserver.conf index b69ca20..5d96ff9 100644 --- a/apiserver/config/default/apiserver.conf +++ b/apiserver/config/default/apiserver.conf @@ -80,8 +80,10 @@ } # # A list of fixed users +# # Note: password may be bcrypt-hashed (generate using `python -c 'import bcrypt; print(bcrypt.hashpw("password", bcrypt.gensalt()))'`) # fixed_users { # enabled: true +# pass_hashed: false # users: [ # { # username: "john" diff --git a/apiserver/requirements.txt b/apiserver/requirements.txt index 1d98919..fd567b9 100644 --- a/apiserver/requirements.txt +++ b/apiserver/requirements.txt @@ -1,4 +1,5 @@ attrs>=19.1.0 +bcrypt>=3.1.4 boltons>=19.1.0 boto3==1.14.13 dpath>=1.4.2,<2.0 diff --git a/apiserver/service_repo/auth/auth.py b/apiserver/service_repo/auth/auth.py index 53af968..fa3c1c5 100644 --- a/apiserver/service_repo/auth/auth.py +++ b/apiserver/service_repo/auth/auth.py @@ -1,6 +1,7 @@ import base64 from datetime import datetime +import bcrypt import jwt from mongoengine import Q @@ -31,8 +32,8 @@ def get_auth_func(auth_type): def authorize_token(jwt_token, *_, **__): - """ Validate token against service/endpoint and requests data (dicts). - Returns a parsed token object (auth payload) + """Validate token against service/endpoint and requests data (dicts). + Returns a parsed token object (auth payload) """ try: return Token.from_encoded_token(jwt_token) @@ -51,13 +52,15 @@ def authorize_token(jwt_token, *_, **__): def authorize_credentials(auth_data, service, action, call_data_items): - """ Validate credentials against service/action and request data (dicts). - Returns a new basic object (auth payload) + """Validate credentials against service/action and request data (dicts). + Returns a new basic object (auth payload) """ try: - access_key, _, secret_key = base64.b64decode(auth_data.encode()).decode('latin-1').partition(':') + access_key, _, secret_key = ( + base64.b64decode(auth_data.encode()).decode("latin-1").partition(":") + ) except Exception as e: - log.exception('malformed credentials') + log.exception("malformed credentials") raise errors.unauthorized.BadCredentials(str(e)) query = Q(credentials__match=Credentials(key=access_key, secret=secret_key)) @@ -67,18 +70,32 @@ def authorize_credentials(auth_data, service, action, call_data_items): if FixedUser.enabled(): fixed_user = FixedUser.get_by_username(access_key) if fixed_user: - if secret_key != fixed_user.password: - raise errors.unauthorized.InvalidCredentials('bad username or password') + if FixedUser.pass_hashed(): + if not compare_secret_key_hash(secret_key, fixed_user.password): + raise errors.unauthorized.InvalidCredentials( + "bad username or password" + ) + else: + if secret_key != fixed_user.password: + raise errors.unauthorized.InvalidCredentials( + "bad username or password" + ) if fixed_user.is_guest and not FixedUser.is_guest_endpoint(service, action): - raise errors.unauthorized.InvalidCredentials('endpoint not allowed for guest') + raise errors.unauthorized.InvalidCredentials( + "endpoint not allowed for guest" + ) query = Q(id=fixed_user.user_id) - with TimingContext("mongo", "user_by_cred"), translate_errors_context('authorizing request'): + with TimingContext("mongo", "user_by_cred"), translate_errors_context( + "authorizing request" + ): user = User.objects(query).first() if not user: - raise errors.unauthorized.InvalidCredentials('failed to locate provided credentials') + raise errors.unauthorized.InvalidCredentials( + "failed to locate provided credentials" + ) if not fixed_user: # In case these are proper credentials, update last used time @@ -87,13 +104,18 @@ def authorize_credentials(auth_data, service, action, call_data_items): ) with TimingContext("mongo", "company_by_id"): - company = Company.objects(id=user.company).only('id', 'name').first() + company = Company.objects(id=user.company).only("id", "name").first() if not company: - raise errors.unauthorized.InvalidCredentials('invalid user company') + raise errors.unauthorized.InvalidCredentials("invalid user company") - identity = Identity(user=user.id, company=user.company, role=user.role, - user_name=user.name, company_name=company.name) + identity = Identity( + user=user.id, + company=user.company, + role=user.role, + user_name=user.name, + company_name=company.name, + ) basic = Basic(user_key=access_key, identity=identity) @@ -110,3 +132,13 @@ def authorize_impersonation(user, identity, service, action, call): raise errors.unauthorized.InvalidCredentials("invalid user company") return Payload(auth_type=None, identity=identity) + + +def compare_secret_key_hash(secret_key: str, hashed_secret: str) -> bool: + """ + Compare hash for the passed secret key with the passed hash + :return: True if equal. Otherwise False + """ + return bcrypt.checkpw( + secret_key.encode(), base64.b64decode(hashed_secret.encode("ascii")) + ) diff --git a/apiserver/service_repo/auth/fixed_user.py b/apiserver/service_repo/auth/fixed_user.py index e62ca69..32694d2 100644 --- a/apiserver/service_repo/auth/fixed_user.py +++ b/apiserver/service_repo/auth/fixed_user.py @@ -32,6 +32,10 @@ class FixedUser: def guest_enabled(cls): return cls.enabled() and config.get("services.auth.fixed_users.guest.enabled", False) + @classmethod + def pass_hashed(cls): + return config.get("apiserver.auth.fixed_users.pass_hashed", False) + @classmethod def validate(cls): if not cls.enabled():