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

View file

@ -1,8 +1,6 @@
# scopes.api.auth user
from fastapi import APIRouter
import jwt
from pwdlib import PasswordHash
from pydantic import BaseModel, Field
from typing import List
@ -15,25 +13,9 @@ class User(BaseModel):
login: str
display_name: str
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)
# 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
router = APIRouter()

View file

@ -9,12 +9,8 @@ from scopes import config
from scopes.storage import common
type Storage = Annotated[common.Storage, Depends(storage)]
type Auth = Annotated[str, Depends(authenticator)]
def storage():
stf = config.StorageFactory(config.settings)
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.responses import HTMLResponse
import transaction # type: ignore
import transaction
from scopes.api import tracking
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 scopes.storage.db.postgres import StorageFactory

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# scopes.storage.folder
from zope.interface import implementer # type: ignore
from zope.interface import implementer
from scopes.interfaces import IContainer, IReference
from scopes.storage.common import registerContainerClass
@ -22,7 +22,7 @@ class DummyFolder(dict):
@implementer(IContainer, IReference)
class Folder(Track):
headFields = ('parent', 'name', 'ref')
headFields = ['parent', 'name', 'ref']
prefix = 'fldr'
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.
"""
import transaction # type: ignore
import transaction
_not_found = object()

View file

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

View file

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

View file

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

View file

@ -61,15 +61,10 @@ def test_tracking(storage, caplog):
def test_user(storage, caplog):
users = storage.create(st_user.Users)
pw = 'secret'
pw_hash = api_user.hashPassword(pw)
u01 = st_user.User('jim', 'jim', data=dict(display_name='Jim', pw_hash=pw_hash))
u01 = st_user.User('jim', 'jim', data=dict(display_name='Jim'))
u01id = users.save(u01)
storage.commit()
with TestClient(app) as clt:
caplog.set_level(config.settings.log_level)
users = clt.get('/users').json()
assert len(users) == 1
u01i = users[0]
assert u01i.get(pw_hash) is None