fixes and improvements, make mypy happy, prepayre auth stuff
This commit is contained in:
parent
12c099b06b
commit
7d309491fc
13 changed files with 50 additions and 24 deletions
|
|
@ -42,3 +42,6 @@ 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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# scopes.api.auth user
|
# scopes.api.auth user
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
import jwt
|
||||||
from pwdlib import PasswordHash
|
from pwdlib import PasswordHash
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
@ -17,7 +18,7 @@ class User(BaseModel):
|
||||||
pw_hash: str | None = Field(exclude=True, default=None)
|
pw_hash: str | None = Field(exclude=True, default=None)
|
||||||
groups: List[str] = Field(default_factory=list)
|
groups: List[str] = Field(default_factory=list)
|
||||||
|
|
||||||
# password handling
|
# password handling, authentication
|
||||||
|
|
||||||
pw_hash = PasswordHash.recommended()
|
pw_hash = PasswordHash.recommended()
|
||||||
|
|
||||||
|
|
@ -27,6 +28,12 @@ def hashPassword(pw):
|
||||||
def verifyPassword(plain, hashed):
|
def verifyPassword(plain, hashed):
|
||||||
return pw_hash.verify(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()
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,12 @@ 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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
import transaction # type: ignore
|
||||||
|
|
||||||
from scopes.api import tracking
|
from scopes.api import tracking
|
||||||
from scopes.api.auth import user
|
from scopes.api.auth import user
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# py-scopes/apidemo/config.py
|
# scopes.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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,9 @@ 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,17 @@
|
||||||
|
|
||||||
"""Core classes for concept map structure."""
|
"""Core classes for concept map structure."""
|
||||||
|
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer # type: ignore
|
||||||
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.tracking import Container, Track
|
from scopes.storage import tracking
|
||||||
|
|
||||||
defaultPredicate = 'standard'
|
defaultPredicate = 'standard'
|
||||||
|
|
||||||
|
|
||||||
class Concept(Track):
|
class Concept(tracking.Track):
|
||||||
|
|
||||||
headFields = ['name']
|
headFields: tuple[str, ...] = ('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(Track):
|
||||||
rels.save(Triple(self.uid, child.uid, predicate))
|
rels.save(Triple(self.uid, child.uid, predicate))
|
||||||
|
|
||||||
|
|
||||||
class Concepts(Container):
|
class Concepts(tracking.Container):
|
||||||
|
|
||||||
insertOnChange = False
|
insertOnChange = False
|
||||||
indexes = None
|
indexes: tracking.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, Track):
|
if isinstance(v, tracking.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(Track):
|
class Triple(tracking.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(Track):
|
||||||
|
|
||||||
|
|
||||||
@registerContainerClass
|
@registerContainerClass
|
||||||
class Rels(Container):
|
class Rels(tracking.Container):
|
||||||
|
|
||||||
itemFactory = Triple
|
itemFactory = Triple
|
||||||
indexes = [('first', 'predicate', 'second'),
|
indexes = [('first', 'predicate', 'second'),
|
||||||
|
|
@ -99,7 +99,7 @@ class Rels(Container):
|
||||||
@implementer(IContainer)
|
@implementer(IContainer)
|
||||||
class Type(Concept):
|
class Type(Concept):
|
||||||
|
|
||||||
headFields = ['name', 'tprefix']
|
headFields = ('name', 'tprefix')
|
||||||
prefix = 'type'
|
prefix = 'type'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -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
|
import transaction # type: ignore
|
||||||
from zope.sqlalchemy import register, mark_changed
|
from zope.sqlalchemy import register, mark_changed # type: ignore
|
||||||
|
|
||||||
from scopes.storage.common import StorageFactory as BaseStorageFactory
|
from scopes.storage.common import StorageFactory as BaseStorageFactory
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# scopes.storage.folder
|
# scopes.storage.folder
|
||||||
|
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer # type: ignore
|
||||||
|
|
||||||
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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
import transaction # type: ignore
|
||||||
|
|
||||||
_not_found = object()
|
_not_found = object()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ 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):
|
||||||
|
|
||||||
|
|
@ -56,7 +57,7 @@ class Record(shape.Record):
|
||||||
|
|
||||||
class Track(Record):
|
class Track(Record):
|
||||||
|
|
||||||
headFields = ('taskId', 'userName')
|
headFields: tuple[str, ...] = ('taskId', 'userName')
|
||||||
prefix = 'rec'
|
prefix = 'rec'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -66,8 +67,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 = None # default, will be overwritten by registerContainerClass()
|
indexes: Indexes = None # is set in registerContainerClass()
|
||||||
#indexes = [('username',), ('taskid', 'username')] # or put explicitly in class
|
#indexes = [('username',), ('taskid', 'username')] # explicit setting in subclass
|
||||||
|
|
||||||
table = None
|
table = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ class User(concept.Concept):
|
||||||
prefix = 'user'
|
prefix = 'user'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@registerContainerClass
|
@registerContainerClass
|
||||||
class Users(concept.Concepts):
|
class Users(concept.Concepts):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,13 @@ DBUSER=testuser
|
||||||
DBPASSWORD=secret
|
DBPASSWORD=secret
|
||||||
DBSCHEMA=testing
|
DBSCHEMA=testing
|
||||||
|
|
||||||
OIDC_PROVIDER=https://oidc.example.com
|
AUTHENTICATOR=testing
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue