Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate passlib #368

Merged
merged 19 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ jobs:
python -m pip install --upgrade pip
pip install bson
pip install pymongo
pip install bcrypt
pip install argon2
pip install jwcrypto
pip install passlib
pip install git+https://github.com/TeskaLabs/asab.git#egg=asab[encryption]

- name: Test with unittest
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
## v24.17

### Pre-releases
- `v24.17-alpha4`
- `v24.17-alpha3`
- `v24.17-alpha2`
- `v24.17-beta1`
- `v24.17-alpha1`

### Fix
- Default provisioning tenant name mst pass validation (#368, `v24.17-alpha4`)
- Fix the initialization and updating of built-in resources (#363, `v24.06-alpha15`)
- Fix searching credentials with multiple filters (#362, `v24.06-alpha14`)

Expand All @@ -20,6 +22,10 @@
- Listing assigned tenants and roles no longer requires resource authorization (#348, `v24.06-alpha13`)
- List credentials from authorized tenant only (#348, `v24.06-alpha13`)

### Refactoring
- Deprecate passlib (#368, `v24.17-alpha4`)
- Utility functions for password verification (#368, `v24.17-alpha4`)

---


Expand Down
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ RUN apk add --no-cache \
cryptography \
jwcrypto>=0.9.1 \
fastjsonschema \
passlib \
bcrypt \
argon2_cffi \
python-ldap \
Expand Down
9 changes: 4 additions & 5 deletions seacatauth/client/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import secrets
import typing
import urllib.parse
import passlib.hash

import asab.storage.exceptions
import asab.exceptions

from seacatauth.client import exceptions
from asab.utils import convert_to_seconds

from . import exceptions
from .. import generic
from ..events import EventTypes

#
Expand Down Expand Up @@ -403,7 +402,7 @@ async def reset_secret(self, client_id: str):
client = await self.get(client_id)
upsertor = self.StorageService.upsertor(self.ClientCollection, obj_id=client_id, version=client["_v"])
client_secret, client_secret_expires_at = self._generate_client_secret()
client_secret_hash = passlib.hash.argon2.hash(client_secret)
client_secret_hash = generic.argon2_hash(client_secret)
upsertor.set("__client_secret", client_secret_hash)
if client_secret_expires_at is not None:
upsertor.set("client_secret_expires_at", client_secret_expires_at)
Expand Down Expand Up @@ -517,7 +516,7 @@ async def authenticate_client(self, client: dict, client_id: str, client_secret:
and client["client_secret_expires_at"] < datetime.datetime.now(datetime.timezone.utc):
raise exceptions.ClientError("Expired client secret.", client_id=client["_id"])

if not passlib.hash.argon2.verify(client_secret, client_secret_hash):
if not generic.argon2_verify(client_secret_hash, client_secret):
raise exceptions.ClientError("Incorrect client secret.", client_id=client["_id"])
else:
return True
Expand Down
5 changes: 2 additions & 3 deletions seacatauth/communication/sms_smsbranacz.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import secrets
import aiohttp
import asab
import passlib.hash
import hashlib

from . import CommunicationProviderABC

Expand Down Expand Up @@ -81,10 +81,9 @@ async def send_message(self, *, phone, message_body):

time = datetime.datetime.now(datetime.timezone.utc).strftime(self.TimestampFormat)
salt = secrets.token_urlsafe(16)
auth = passlib.hash.hex_md5.hash(self.Password + time + salt)
url_params["time"] = time
url_params["salt"] = salt
url_params["auth"] = auth
url_params["auth"] = hashlib.md5((self.Password + time + salt).encode("utf-8")).hexdigest()

if self.MockMode:
L.log(
Expand Down
27 changes: 21 additions & 6 deletions seacatauth/credentials/providers/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import asab

from ... import generic

#

L = logging.getLogger(__name__)
Expand Down Expand Up @@ -43,10 +45,10 @@ def get_info(self) -> dict:


async def locate(self, ident: str, ident_fields: dict = None, login_dict: dict = None) -> str:
'''
"""
Locate credentials based on the vague 'ident', which could be the username, password, phone number etc.
Return credentials_id or return None if not found.
'''
"""
return None

async def get_by(self, key: str, value) -> Optional[dict]:
Expand All @@ -56,9 +58,9 @@ async def get_by(self, key: str, value) -> Optional[dict]:
return None

async def get_login_descriptors(self, credentials_id) -> list:
'''
"""
Create a descriptor for the allowed login configurations
'''
"""
return []


Expand All @@ -69,12 +71,12 @@ async def get(self, credentials_id, include=None) -> Optional[dict]:

@abc.abstractmethod
async def count(self, filtr: str = None) -> int:
'''
"""
Non-authoritative count of the credentials managed by the provider.
It is used for indicative information on the UI.

Should return None if unable to count credentials managed.
'''
"""
return None


Expand All @@ -91,6 +93,19 @@ async def authenticate(self, credentials_id: str, credentials: dict) -> bool:
return False


def _verify_password(self, hash: str, password: str) -> bool:
"""
Check if the password matches the hash.
"""
if hash.startswith("$2b$") or hash.startswith("$2a$") or hash.startswith("$2y$"):
return generic.bcrypt_verify(hash, password)
elif hash.startswith("$argon2id$"):
return generic.argon2_verify(hash, password)
else:
L.warning("Unknown password hash function: {}".format(hash[:4]))
return False



class EditableCredentialsProviderABC(CredentialsProviderABC):

Expand Down
10 changes: 4 additions & 6 deletions seacatauth/credentials/providers/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import logging
from typing import Optional

from passlib.hash import bcrypt

import asab
from .abc import EditableCredentialsProviderABC
from ... import generic

#

Expand All @@ -23,7 +22,6 @@ def __init__(self, app, service_name='seacatauth.credentials.dict'):
super().__init__(app, service_name)

def create_provider(self, provider_id, config_section_name):
# TODO: Check bcrypt.get_backend() - see https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#index-0
return DictCredentialsProvider(provider_id, config_section_name)


Expand Down Expand Up @@ -77,7 +75,7 @@ async def create(self, credentials: dict) -> Optional[str]:
if "phone" in credentials:
credentials_object["phone"] = credentials["phone"]
if "password" in credentials:
credentials_object["__password"] = bcrypt.hash(credentials["password"].encode("utf-8"))
credentials_object["__password"] = generic.bcrypt_hash(credentials["password"])

self.Dictionary[credentials_id] = credentials_object
return "{}:{}:{}".format(self.Type, self.ProviderID, credentials_id)
Expand All @@ -94,7 +92,7 @@ async def update(self, credentials_id, update: dict) -> Optional[str]:
# Update the password
if "password" in update:
new_pwd = update.pop("password")
credentials["__password"] = bcrypt.hash(new_pwd.encode('utf-8'))
credentials["__password"] = generic.bcrypt_hash(new_pwd)

for k, v in update.items():
credentials[k] = v
Expand Down Expand Up @@ -154,7 +152,7 @@ async def authenticate(self, credentials_id: str, credentials: dict) -> bool:
if credentials_db is None:
return False

if bcrypt.verify(password, credentials_db["__password"]):
if generic.bcrypt_verify(credentials_db["__password"], password):
return True

return False
Expand Down
54 changes: 36 additions & 18 deletions seacatauth/credentials/providers/htpasswd.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import logging
import functools
import os
from typing import Optional

from passlib.apache import HtpasswdFile

import asab
from .abc import CredentialsProviderABC

Expand Down Expand Up @@ -33,16 +32,29 @@ class HTPasswdCredentialsProvider(CredentialsProviderABC):

def __init__(self, provider_id, config_section_name):
super().__init__(provider_id, config_section_name)
self.HT = HtpasswdFile(self.Config['path'])
self._Path = self.Config["path"]
self._Dict = {}
self._MTime = 0
self._refresh()

def _refresh(self):
if self._Dict and self._MTime == os.path.getmtime(self._Path):
# Reload not needed
return

with open(self._Path, "r") as f:
for line in f:
username, password = line.strip().split(":", 1)
self._Dict[username] = password


async def locate(self, ident: str, ident_fields: dict = None, login_dict: dict = None) -> str:
# TODO: Implement ident_fields support
'''
"""
Locate search for the exact match of provided ident and the username in the htpasswd file
'''
self.HT.load_if_changed()
if ident not in frozenset(self.HT.users()):
"""
self._refresh()
if ident not in self._Dict:
return None
return "{}:{}:{}".format(self.Type, self.ProviderID, ident)

Expand All @@ -66,10 +78,10 @@ async def get(self, credentials_id, include=None) -> Optional[dict]:
if not credentials_id.startswith(prefix):
raise KeyError("Credentials '{}' not found".format(credentials_id))

self.HT.load_if_changed()
self._refresh()

username = credentials_id[len(prefix):]
if username not in frozenset(self.HT.users()):
if username not in self._Dict:
raise KeyError("Credentials '{}' not found".format(credentials_id))

return {
Expand All @@ -91,28 +103,33 @@ async def authenticate(self, credentials_id: str, credentials: dict) -> bool:

password = credentials.get('password', '')

self.HT.load_if_changed()
if self.HT.check_password(username, password):
self._refresh()
password_hash = self._Dict.get(username)
if not password_hash:
return False

if self._verify_password(password_hash, password):
return True

return False


async def count(self, filtr=None) -> int:
self._refresh()
if filtr is None:
return len(self.HT.users())
return len(self._Dict)

def filter_fnct(x, y):
if filtr in y:
return x + 1
else:
return x

return functools.reduce(filter_fnct, self.HT.users(), 0)
return functools.reduce(filter_fnct, self._Dict.keys(), 0)


async def search(self, filter: dict = None, **kwargs) -> list:
# TODO: Implement filtering and pagination
self._refresh()
if filter is not None:
return []
prefix = "{}:{}:".format(self.Type, self.ProviderID)
Expand All @@ -122,19 +139,20 @@ async def search(self, filter: dict = None, **kwargs) -> list:
'_type': self.Type,
'_provider_id': self.ProviderID,
'username': username,
} for username in self.HT.users()
} for username in self._Dict
]


async def iterate(self, offset: int = 0, limit: int = -1, filtr: str = None):
self._refresh()
prefix = "{}:{}:".format(self.Type, self.ProviderID)

if filtr is None:
arr = self.HT.users()
arr = list(self._Dict.keys())
else:
arr = [u for u in self.HT.users() if filtr in u]
arr = [u for u in self._Dict if filtr in u]

for username in arr[offset:None if limit == -1 else limit + offset]:
for username in arr[offset: None if limit == -1 else limit + offset]:
yield {
'_id': prefix + username,
'_type': self.Type,
Expand Down
5 changes: 2 additions & 3 deletions seacatauth/credentials/providers/m2m_mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

import pymongo

from passlib.hash import bcrypt
from .mongodb import MongoDBCredentialsProvider

from ... import generic
from ...events import EventTypes

#
Expand Down Expand Up @@ -80,7 +79,7 @@ async def create(self, credentials: dict) -> Optional[str]:
u = self.MongoDBStorageService.upsertor(self.CredentialsCollection, obj_id)

u.set("username", credentials["username"])
u.set("__password", bcrypt.hash(credentials["password"].encode("utf-8")))
u.set("__password", generic.bcrypt_hash(credentials["password"]))

credentials_id = await u.execute(event_type=EventTypes.M2M_CREDENTIALS_CREATED)

Expand Down
Loading
Loading