Compare commits

..

No commits in common. "7d309491fc545dae4f6c46e8b85bfeed7a754cb4" and "e0a53dce68451944f2aee3cb433b870bc8d086ee" have entirely different histories.

14 changed files with 26 additions and 68 deletions

View file

@ -29,7 +29,7 @@ zope = [
"zope.traversing", "zope.traversing",
] ]
api = ["fastapi[standard]"] api = ["fastapi[standard]"]
auth = ["authlib", "pyjwt[crypto]", "cryptography", "pwdlib[argon2]", "requests"] auth = ["authlib", "pyjwt[crypto]", "cryptography", "requests"]
zopetest = ["zope.testrunner"] zopetest = ["zope.testrunner"]
apitest = ["pytest", "httpx2"] apitest = ["pytest", "httpx2"]
@ -42,6 +42,3 @@ testpaths = ["tests"]
#python_files = ["test_api.py"] #python_files = ["test_api.py"]
# use .pytest.ini file for local test config # use .pytest.ini file for local test config
[tool.mypy]
follow_imports = 'skip'

View file

@ -1,8 +1,6 @@
# scopes.api.auth user # scopes.api.auth user
from fastapi import APIRouter from fastapi import APIRouter
import jwt
from pwdlib import PasswordHash
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import List from typing import List
@ -15,25 +13,9 @@ class User(BaseModel):
login: str login: str
display_name: str display_name: str
email: str | None = None email: str | None = None
pw_hash: str | None = Field(exclude=True, default=None) pw_hash: str | None = None
groups: List[str] = Field(default_factory=list) groups: List[str] = Field(default_factory=list)
# password handling, authentication
pw_hash = PasswordHash.recommended()
def hashPassword(pw):
return pw_hash.hash(pw)
def verifyPassword(plain, hashed):
return pw_hash.verify(plain, hashed)
async def authenticate(storage: deps.Storage, token: deps.Auth):
payload = jwt.decode(token, settings.jwt_key, algorithms=[settings.jwt_algorithm])
userName = payload.get('sub')
user = getUser(storage, userName)
return user
# routes # routes
router = APIRouter() router = APIRouter()

View file

@ -9,12 +9,8 @@ from scopes import config
from scopes.storage import common from scopes.storage import common
type Storage = Annotated[common.Storage, Depends(storage)] type Storage = Annotated[common.Storage, Depends(storage)]
type Auth = Annotated[str, Depends(authenticator)]
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 authenticator():
return config.Authenticator()

View file

@ -6,7 +6,7 @@ if '' not in sys.path:
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
import transaction # type: ignore import transaction
from scopes.api import tracking from scopes.api import tracking
from scopes.api.auth import user from scopes.api.auth import user

View file

@ -1,4 +1,4 @@
# scopes.config.py # py-scopes/apidemo/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from scopes.storage.db.postgres import StorageFactory from scopes.storage.db.postgres import StorageFactory

View file

@ -55,9 +55,3 @@ class Record:
def toObject(self, cls): def toObject(self, cls):
return cls(**self.asDict(flatten=True)) return cls(**self.asDict(flatten=True))
@classmethod
def fromObject(cls, obj):
inp = obj.model_dump()
headValues = [inp.pop(hf) for hf in cls.headFields]
return cls(*headValues, data=inp)

View file

@ -2,17 +2,17 @@
"""Core classes for concept map structure.""" """Core classes for concept map structure."""
from zope.interface import implementer # type: ignore from zope.interface import implementer
from scopes.interfaces import IContainer from scopes.interfaces import IContainer
from scopes.storage.common import registerContainerClass, registry from scopes.storage.common import registerContainerClass, registry
from scopes.storage import tracking from scopes.storage.tracking import Container, Track
defaultPredicate = 'standard' defaultPredicate = 'standard'
class Concept(tracking.Track): class Concept(Track):
headFields: tuple[str, ...] = ('name',) headFields = ['name']
def parents(self, predicate=None): def parents(self, predicate=None):
return (r.getFirst() for r in self.parentRels(predicate)) return (r.getFirst() for r in self.parentRels(predicate))
@ -34,17 +34,17 @@ class Concept(tracking.Track):
rels.save(Triple(self.uid, child.uid, predicate)) rels.save(Triple(self.uid, child.uid, predicate))
class Concepts(tracking.Container): class Concepts(Container):
insertOnChange = False insertOnChange = False
indexes: tracking.Indexes = None indexes = None
def queryRels(self, **crit): def queryRels(self, **crit):
#pred = crit.get(predicate) #pred = crit.get(predicate)
#if pred is not None and isinstance(pred, ('string', 'bytes')): #if pred is not None and isinstance(pred, ('string', 'bytes')):
# crit['predicate'] = self.storage.getContainer(Predicate).queryLast(name=pred) # crit['predicate'] = self.storage.getContainer(Predicate).queryLast(name=pred)
for k, v in crit.items(): for k, v in crit.items():
if isinstance(v, tracking.Track): if isinstance(v, Track):
crit[k] = v.uid crit[k] = v.uid
rels = self.storage.getContainer(Triple) rels = self.storage.getContainer(Triple)
return rels.query(**crit) return rels.query(**crit)
@ -69,9 +69,9 @@ def storePredicate(storage, name):
preds.save(Predicate(name)) preds.save(Predicate(name))
class Triple(tracking.Track): class Triple(Track):
headFields = ('first', 'second', 'predicate') headFields = ['first', 'second', 'predicate']
prefix = 'rel' prefix = 'rel'
def getFirst(self): def getFirst(self):
@ -85,7 +85,7 @@ class Triple(tracking.Track):
@registerContainerClass @registerContainerClass
class Rels(tracking.Container): class Rels(Container):
itemFactory = Triple itemFactory = Triple
indexes = [('first', 'predicate', 'second'), indexes = [('first', 'predicate', 'second'),
@ -99,7 +99,7 @@ class Rels(tracking.Container):
@implementer(IContainer) @implementer(IContainer)
class Type(Concept): class Type(Concept):
headFields = ('name', 'tprefix') headFields = ['name', 'tprefix']
prefix = 'type' prefix = 'type'
@property @property

View file

@ -6,8 +6,8 @@ from sqlalchemy import create_engine
from sqlalchemy import BigInteger from sqlalchemy import BigInteger
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm import scoped_session, sessionmaker
import transaction # type: ignore import transaction
from zope.sqlalchemy import register, mark_changed # type: ignore from zope.sqlalchemy import register, mark_changed
from scopes.storage.common import StorageFactory as BaseStorageFactory from scopes.storage.common import StorageFactory as BaseStorageFactory

View file

@ -1,6 +1,6 @@
# scopes.storage.folder # scopes.storage.folder
from zope.interface import implementer # type: ignore from zope.interface import implementer
from scopes.interfaces import IContainer, IReference from scopes.interfaces import IContainer, IReference
from scopes.storage.common import registerContainerClass from scopes.storage.common import registerContainerClass
@ -22,7 +22,7 @@ class DummyFolder(dict):
@implementer(IContainer, IReference) @implementer(IContainer, IReference)
class Folder(Track): class Folder(Track):
headFields = ('parent', 'name', 'ref') headFields = ['parent', 'name', 'ref']
prefix = 'fldr' prefix = 'fldr'
def values(self): def values(self):

View file

@ -6,7 +6,7 @@ in order to store attribute values in a SQL database.
This is currently in concept and exploration state. This is currently in concept and exploration state.
""" """
import transaction # type: ignore import transaction
_not_found = object() _not_found = object()

View file

@ -15,7 +15,6 @@ from sqlalchemy import and_
from scopes import shape from scopes import shape
from scopes.storage.common import registerContainerClass from scopes.storage.common import registerContainerClass
type Indexes = list[tuple[str, ...]] | None
class Record(shape.Record): class Record(shape.Record):
@ -57,7 +56,7 @@ class Record(shape.Record):
class Track(Record): class Track(Record):
headFields: tuple[str, ...] = ('taskId', 'userName') headFields = ('taskId', 'userName')
prefix = 'rec' prefix = 'rec'
@ -67,8 +66,8 @@ class Container:
itemFactory = Track itemFactory = Track
tableName = 'tracks' tableName = 'tracks'
insertOnChange = True # always insert new track when data are changed insertOnChange = True # always insert new track when data are changed
indexes: Indexes = None # is set in registerContainerClass() indexes = None # default, will be overwritten by registerContainerClass()
#indexes = [('username',), ('taskid', 'username')] # explicit setting in subclass #indexes = [('username',), ('taskid', 'username')] # or put explicitly in class
table = None table = None

View file

@ -9,6 +9,7 @@ class User(concept.Concept):
prefix = 'user' prefix = 'user'
@registerContainerClass @registerContainerClass
class Users(concept.Concepts): class Users(concept.Concepts):

View file

@ -9,13 +9,7 @@ DBUSER=testuser
DBPASSWORD=secret DBPASSWORD=secret
DBSCHEMA=testing DBSCHEMA=testing
AUTHENTICATOR=testing OIDC_PROVIDER=https://oidc.example.com
# openssl rand -hex 32:
JWT_KEY=391a521ee5ba2b6ce67b8fdbb9f0ca88f40a87d3bc1e82974381b647bb0fc776
JWT_ALGORITHM=HS256
OIDC_PROVIDER=http://oidc.test
OIDC_CLIENT_ID=12345 OIDC_CLIENT_ID=12345
OIDC_COOKIE_CRYPT=secret_crypt OIDC_COOKIE_CRYPT=secret_crypt
OIDC_ORGANIZATION_ID=12346 OIDC_ORGANIZATION_ID=12346

View file

@ -61,15 +61,10 @@ def test_tracking(storage, caplog):
def test_user(storage, caplog): def test_user(storage, caplog):
users = storage.create(st_user.Users) users = storage.create(st_user.Users)
pw = 'secret' u01 = st_user.User('jim', 'jim', data=dict(display_name='Jim'))
pw_hash = api_user.hashPassword(pw)
u01 = st_user.User('jim', 'jim', data=dict(display_name='Jim', pw_hash=pw_hash))
u01id = users.save(u01) u01id = users.save(u01)
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)
users = clt.get('/users').json() users = clt.get('/users').json()
assert len(users) == 1 assert len(users) == 1
u01i = users[0]
assert u01i.get(pw_hash) is None