Compare commits
3 commits
fc0616ce6a
...
342f48042c
| Author | SHA1 | Date | |
|---|---|---|---|
| 342f48042c | |||
| 8963c2ac87 | |||
| a139fd784e |
5 changed files with 58 additions and 29 deletions
|
|
@ -9,7 +9,7 @@ class Settings(BaseSettings):
|
||||||
#, env_prefix='SCOPES_')
|
#, env_prefix='SCOPES_')
|
||||||
auth_method: str = 'oauth2'
|
auth_method: str = 'oauth2'
|
||||||
jwt_key: str | None = None
|
jwt_key: str | None = None
|
||||||
jwt_atlgorithm: str = 'HS256'
|
jwt_algorithm: str = 'HS256'
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
# scopes.api.auth user
|
# scopes.api.auth user
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, Body
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
import jwt
|
import jwt
|
||||||
from pwdlib import PasswordHash
|
from pwdlib import PasswordHash
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Annotated, List
|
from typing import Annotated, List
|
||||||
|
|
||||||
from scopes.api import deps
|
from scopes.api import deps
|
||||||
from scopes.config import settings
|
from scopes.api.auth import config
|
||||||
from scopes.storage import user as st_user
|
from scopes.storage import user as st_user
|
||||||
from scopes.storage.common import Storage
|
from scopes.storage.common import Storage
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ class User(UserBase):
|
||||||
timeStamp: datetime | None = None
|
timeStamp: datetime | None = None
|
||||||
pw_hash: str | None = Field(exclude=True, default=None)
|
pw_hash: str | None = Field(exclude=True, default=None)
|
||||||
|
|
||||||
class UserIn(UserBase):
|
class UserEdit(UserBase):
|
||||||
password: str | None = None
|
password: str | None = None
|
||||||
|
|
||||||
def hashPassword(self) -> str | None:
|
def hashPassword(self) -> str | None:
|
||||||
|
|
@ -33,6 +34,10 @@ class UserIn(UserBase):
|
||||||
|
|
||||||
# password handling, authentication
|
# password handling, authentication
|
||||||
|
|
||||||
|
def dummy_auth() -> User:
|
||||||
|
return User(name='tester', login='tester', display_name='Tester')
|
||||||
|
config.auth_methods['testing'] = dummy_auth
|
||||||
|
|
||||||
pw_hash = PasswordHash.recommended()
|
pw_hash = PasswordHash.recommended()
|
||||||
|
|
||||||
def hashPassword(pw):
|
def hashPassword(pw):
|
||||||
|
|
@ -41,31 +46,42 @@ def hashPassword(pw):
|
||||||
def verifyPassword(plain, hashed):
|
def verifyPassword(plain, hashed):
|
||||||
return pw_hash.verify(plain, hashed)
|
return pw_hash.verify(plain, hashed)
|
||||||
|
|
||||||
def getUser(storage: Storage, userName: str) -> User:
|
security = HTTPBasic()
|
||||||
users = storage.create(st_user.Users)
|
|
||||||
user = users.queryLast(name=userName)
|
|
||||||
return user.toObject(User)
|
|
||||||
|
|
||||||
async def authenticate(storage: deps.Storage, token: deps.Auth):
|
type Credentials = Annotated[HTTPBasicCredentials, Depends(security)]
|
||||||
payload = jwt.decode(token, settings.jwt_key, algorithms=[settings.jwt_algorithm])
|
|
||||||
|
def basicAuth(users: deps.Users, cred: Credentials) -> User:
|
||||||
|
tr = users.queryLast(login=cred.username)
|
||||||
|
users.storage.commit()
|
||||||
|
user = tr.toObject(User)
|
||||||
|
if verifyPassword(cred.password, user.pw_hash):
|
||||||
|
return user
|
||||||
|
raise HTTPException(status_code=401, detail='credentials',
|
||||||
|
headers={"WWW-Authenticate": "Basic"})
|
||||||
|
config.auth_methods['basic'] = basicAuth
|
||||||
|
|
||||||
|
def userFromToken(users: deps.Users, token: deps.AuthToken) -> User:
|
||||||
|
payload = jwt.decode(token, config.settings.jwt_key,
|
||||||
|
algorithms=[config.settings.jwt_algorithm])
|
||||||
userName = payload.get('sub')
|
userName = payload.get('sub')
|
||||||
user = getUser(storage, userName)
|
tr = users.queryLast(name=userName)
|
||||||
return user
|
users.storage.commit()
|
||||||
|
return tr.toObject(User)
|
||||||
|
|
||||||
# routes
|
# routes
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get('/users', response_model=list[User])
|
@router.get('/users', response_model=list[User])
|
||||||
async def get_users(storage: deps.Storage):
|
async def get_users(users: deps.Users):
|
||||||
users = storage.create(st_user.Users)
|
|
||||||
result = [tr.toObject(User) for tr in users.query()]
|
result = [tr.toObject(User) for tr in users.query()]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@router.post('/users', response_model=User)
|
@router.post('/users', response_model=User)
|
||||||
async def post_user(user: Annotated[UserIn, Body()], storage: deps.Storage):
|
async def post_user(user: Annotated[UserEdit, Body()], users: deps.Users,
|
||||||
users = storage.create(st_user.Users)
|
auth_user: deps.AuthUser):
|
||||||
tr = st_user.User.fromObject(user)
|
tr = st_user.User.fromObject(user)
|
||||||
users.save(tr)
|
users.save(tr)
|
||||||
|
#print('*** post_user', auth_user, tr)
|
||||||
return tr.toObject(User)
|
return tr.toObject(User)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,30 @@ from typing import Annotated
|
||||||
from scopes import config
|
from scopes import config
|
||||||
from scopes.api.auth import config as auth_config
|
from scopes.api.auth import config as auth_config
|
||||||
from scopes.storage import common
|
from scopes.storage import common
|
||||||
|
from scopes.storage import user as st_user
|
||||||
|
|
||||||
type Storage = Annotated[common.Storage, Depends(storage)]
|
type Storage = Annotated[common.Storage, Depends(storage)]
|
||||||
type Auth = Annotated[str, Depends(auth_method())]
|
type Users = Annotated[st_user.Users, Depends(users)]
|
||||||
|
|
||||||
|
type AuthUser = Annotated[str, Depends(auth_user())]
|
||||||
|
type AuthToken = Annotated[str, Depends(auth_token)]
|
||||||
|
|
||||||
|
# storage
|
||||||
|
|
||||||
def storage():
|
def storage():
|
||||||
stf = config.StorageFactory(config.settings)
|
stf = config.StorageFactory(config.settings)
|
||||||
return stf(config.settings.dbschema)
|
return stf(config.settings.dbschema)
|
||||||
|
|
||||||
def auth_method():
|
def users(storage: Storage):
|
||||||
auth = auth_config.auth_methods.get(auth_config.settings.auth_method)
|
return storage.create(st_user.Users)
|
||||||
return auth
|
|
||||||
|
# auth
|
||||||
|
|
||||||
|
def auth_user():
|
||||||
|
auth = auth_config.auth_methods.get(auth_config.settings.auth_method)
|
||||||
|
if auth is not None:
|
||||||
|
return auth
|
||||||
|
|
||||||
|
def auth_token():
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ DBUSER=testuser
|
||||||
DBPASSWORD=secret
|
DBPASSWORD=secret
|
||||||
DBSCHEMA=testing
|
DBSCHEMA=testing
|
||||||
|
|
||||||
AUTH_METHOD=testing
|
AUTH_METHOD=basic
|
||||||
|
|
||||||
# openssl rand -hex 32:
|
# openssl rand -hex 32:
|
||||||
JWT_KEY=391a521ee5ba2b6ce67b8fdbb9f0ca88f40a87d3bc1e82974381b647bb0fc776
|
JWT_KEY=391a521ee5ba2b6ce67b8fdbb9f0ca88f40a87d3bc1e82974381b647bb0fc776
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,15 @@
|
||||||
"""Tests for the fastapi- (and asyncio-) based stuff"""
|
"""Tests for the fastapi- (and asyncio-) based stuff"""
|
||||||
|
|
||||||
from scopes import config
|
from scopes import config
|
||||||
|
from scopes.api.auth import config as auth_config
|
||||||
config.settings = config.Settings(_env_file='tests/test.env')
|
config.settings = config.Settings(_env_file='tests/test.env')
|
||||||
|
auth_config.settings = auth_config.Settings(_env_file='tests/test.env')
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from httpx2 import ASGITransport, AsyncClient
|
from httpx2 import ASGITransport, AsyncClient, BasicAuth
|
||||||
|
|
||||||
from scopes.api import main
|
from scopes.api import main
|
||||||
from scopes.api.auth import config as auth_config
|
|
||||||
from scopes.api.main import app
|
from scopes.api.main import app
|
||||||
from scopes.storage import tracking, user as st_user
|
from scopes.storage import tracking, user as st_user
|
||||||
from scopes.api.auth import user as api_user
|
from scopes.api.auth import user as api_user
|
||||||
|
|
@ -23,10 +24,6 @@ def storage():
|
||||||
storage.commit()
|
storage.commit()
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
def dummy_auth():
|
|
||||||
return api_user.User(name='tester', login='tester', display_name='Tester')
|
|
||||||
auth_config.auth_methods['testing'] = dummy_auth
|
|
||||||
|
|
||||||
def async_client():
|
def async_client():
|
||||||
return AsyncClient(transport=ASGITransport(app=app), base_url='http://test')
|
return AsyncClient(transport=ASGITransport(app=app), base_url='http://test')
|
||||||
|
|
||||||
|
|
@ -73,13 +70,14 @@ def test_user(storage, caplog):
|
||||||
storage.commit()
|
storage.commit()
|
||||||
with TestClient(app) as clt:
|
with TestClient(app) as clt:
|
||||||
caplog.set_level(config.settings.log_level)
|
caplog.set_level(config.settings.log_level)
|
||||||
|
auth = BasicAuth(username='jim', password='secret')
|
||||||
users = clt.get('/users').json()
|
users = clt.get('/users').json()
|
||||||
assert len(users) == 1
|
assert len(users) == 1
|
||||||
u01i = users[0]
|
u01i = users[0]
|
||||||
assert u01i.get(pw_hash) is None
|
assert u01i.get(pw_hash) is None
|
||||||
inp = {'name': 'jane', 'login': 'jane', 'display_name': 'Jane',
|
inp = {'name': 'jane', 'login': 'jane', 'display_name': 'Jane',
|
||||||
'email': 'jane@xmpl.org', 'password': 'secret'}
|
'email': 'jane@xmpl.org', 'password': 'secret'}
|
||||||
resp = clt.post('/users', json=inp)
|
resp = clt.post('/users', json=inp, auth=auth)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
u02 = resp.json()
|
u02 = resp.json()
|
||||||
assert u02['uid'] == 'user-2'
|
assert u02['uid'] == 'user-2'
|
||||||
|
|
@ -87,7 +85,7 @@ def test_user(storage, caplog):
|
||||||
assert ts02 is not None
|
assert ts02 is not None
|
||||||
del inp['password']
|
del inp['password']
|
||||||
inp['display_name'] = 'Jane Doe'
|
inp['display_name'] = 'Jane Doe'
|
||||||
resp = clt.post('/users', json=inp)
|
resp = clt.post('/users', json=inp, auth=auth)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
u02a = resp.json()
|
u02a = resp.json()
|
||||||
assert u02a['uid'] == 'user-2'
|
assert u02a['uid'] == 'user-2'
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue