Compare commits

...

26 Commits

Author SHA1 Message Date
4e17e6da82 main.py fixed pycryptodome import 2022-12-23 14:09:13 +01:00
843d918e59 added dependencies 2022-12-23 14:08:56 +01:00
952a74cabe Merge branch 'sqlalchemy' into debian
# Conflicts:
#	app/main.py
2022-12-23 13:50:50 +01:00
81608fe497 merged dev into debian 2022-12-23 13:48:48 +01:00
b00a2a032a Merge branch 'dev' into debian
# Conflicts:
#	.gitlab-ci.yml
2022-12-23 13:48:24 +01:00
6b7c70e59a tests improved 2022-12-23 13:42:02 +01:00
332b9b23cd code styling 2022-12-23 13:31:43 +01:00
3d5d728d67 code styling 2022-12-23 13:22:06 +01:00
838e30458d code styling 2022-12-23 13:21:52 +01:00
f539db5933 implemented db_init 2022-12-23 13:17:19 +01:00
6049048bbf fixed test 2022-12-23 11:24:40 +01:00
43d5736f37 code styling & removed comments 2022-12-23 08:22:21 +01:00
e7102c4de6 fixed updates 2022-12-23 08:16:58 +01:00
d1db441df4 removed Auth 2022-12-23 08:16:34 +01:00
d5b51bd83c Merge branch 'dev' into sqlalchemy
# Conflicts:
#	app/main.py
2022-12-23 08:08:35 +01:00
3f71c88d48 added some test 2022-12-23 07:48:47 +01:00
a58549a162 .gitlab-ci.yml - fixed test cert path 2022-12-23 07:43:02 +01:00
2c1c9b63b4 .gitignore 2022-12-23 07:41:23 +01:00
3367977652 .gitlab-ci.yml - fixed cd into test 2022-12-23 07:41:18 +01:00
67ed6108a2 .gitlab-ci.yml - changed test image to bullseye 2022-12-23 07:40:27 +01:00
d5d156e70e .gitlab-ci.yml - create test certificates 2022-12-23 07:38:53 +01:00
906af9430a .gitlab-ci.yml - fixed installing dependencies 2022-12-23 07:36:33 +01:00
3f5e3b16c5 added api tests 2022-12-23 07:35:37 +01:00
9809bbdbd1 bump version to 0.6 2022-12-23 07:16:41 +01:00
a0b9eae15b main.py - fixed wrong "origin_ref" in CodeResponse
- fixed issue
- removed the now unnecessary table "auth"
2022-12-23 06:56:29 +01:00
394180652e migrated from dataset to sqlalchemy 2022-12-22 12:57:06 +01:00
8 changed files with 271 additions and 68 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ venv/
.idea/ .idea/
app/*.sqlite* app/*.sqlite*
app/cert/*.* app/cert/*.*
.pytest_cache

View File

@ -36,6 +36,21 @@ build:docker:
- docker build . --tag ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF} - docker build . --tag ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF}
- docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF} - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_BUILD_REF_NAME}:${CI_BUILD_REF}
test:
image: python:3.10-slim-bullseye
stage: test
variables:
DATABASE: sqlite:///../app/db.sqlite
before_script:
- pip install -r requirements.txt
- pip install pytest httpx
- mkdir -p app/cert
- openssl genrsa -out app/cert/instance.private.pem 2048
- openssl rsa -in app/cert/instance.private.pem -outform PEM -pubout -out app/cert/instance.public.pem
- cd test
script:
- pytest main.py
test:debian: test:debian:
image: debian:bookworm-slim image: debian:bookworm-slim
stage: test stage: test

View File

@ -2,7 +2,7 @@ Package: fastapi-dls
Version: 0.5 Version: 0.5
Architecture: all Architecture: all
Maintainer: Oscar Krause oscar.krause@collinwebdesigns.de Maintainer: Oscar Krause oscar.krause@collinwebdesigns.de
Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-jose, uvicorn, openssl Depends: python3, python3-fastapi, python3-uvicorn, python3-dotenv, python3-dateutil, python3-jose, python3-sqlalchemy, python3-pycryptodome, uvicorn, openssl
Recommends: curl Recommends: curl
Installed-Size: 10240 Installed-Size: 10240
Homepage: https://git.collinwebdesigns.de/oscar.krause/fastapi-dls Homepage: https://git.collinwebdesigns.de/oscar.krause/fastapi-dls

View File

@ -17,16 +17,18 @@ from jose import jws, jwk, jwt
from jose.constants import ALGORITHMS from jose.constants import ALGORITHMS
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse, JSONResponse, HTMLResponse from starlette.responses import StreamingResponse, JSONResponse, HTMLResponse
import dataset from sqlalchemy import create_engine
from Crypto.PublicKey import RSA from sqlalchemy.orm import sessionmaker
from Crypto.PublicKey.RSA import RsaKey from Cryptodome.PublicKey import RSA # Crypto | Cryptodome on Debian
from Cryptodome.PublicKey.RSA import RsaKey # Crypto | Cryptodome on Debian
from orm import Origin, Lease, init as db_init
logger = logging.getLogger() logger = logging.getLogger()
load_dotenv('../version.env') load_dotenv('../version.env')
VERSION, COMMIT, DEBUG = getenv('VERSION', 'unknown'), getenv('COMMIT', 'unknown'), bool(getenv('DEBUG', False)) VERSION, COMMIT, DEBUG = getenv('VERSION', 'unknown'), getenv('COMMIT', 'unknown'), bool(getenv('DEBUG', False))
def load_file(filename) -> bytes: def load_file(filename) -> bytes:
with open(filename, 'rb') as file: with open(filename, 'rb') as file:
content = file.read() content = file.read()
@ -45,7 +47,8 @@ __details = dict(
version=VERSION, version=VERSION,
) )
app, db = FastAPI(**__details), dataset.connect(str(getenv('DATABASE', 'sqlite:///db.sqlite'))) app, db = FastAPI(**__details), create_engine(url=str(getenv('DATABASE', 'sqlite:///db.sqlite')))
db_init(db)
TOKEN_EXPIRE_DELTA = relativedelta(hours=1) # days=1 TOKEN_EXPIRE_DELTA = relativedelta(hours=1) # days=1
LEASE_EXPIRE_DELTA = relativedelta(days=int(getenv('LEASE_EXPIRE_DAYS', 90))) LEASE_EXPIRE_DELTA = relativedelta(days=int(getenv('LEASE_EXPIRE_DAYS', 90)))
@ -53,6 +56,7 @@ LEASE_EXPIRE_DELTA = relativedelta(days=int(getenv('LEASE_EXPIRE_DAYS', 90)))
DLS_URL = str(getenv('DLS_URL', 'localhost')) DLS_URL = str(getenv('DLS_URL', 'localhost'))
DLS_PORT = int(getenv('DLS_PORT', '443')) DLS_PORT = int(getenv('DLS_PORT', '443'))
SITE_KEY_XID = getenv('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000') SITE_KEY_XID = getenv('SITE_KEY_XID', '00000000-0000-0000-0000-000000000000')
INSTANCE_REF = '00000000-0000-0000-0000-000000000000'
INSTANCE_KEY_RSA = load_key(join(dirname(__file__), 'cert/instance.private.pem')) INSTANCE_KEY_RSA = load_key(join(dirname(__file__), 'cert/instance.private.pem'))
INSTANCE_KEY_PUB = load_key(join(dirname(__file__), 'cert/instance.public.pem')) INSTANCE_KEY_PUB = load_key(join(dirname(__file__), 'cert/instance.public.pem'))
@ -93,13 +97,17 @@ async def status(request: Request):
@app.get('/-/origins') @app.get('/-/origins')
async def _origins(request: Request): async def _origins(request: Request):
response = list(map(lambda x: jsonable_encoder(x), db['origin'].all())) session = sessionmaker(bind=db)()
response = list(map(lambda x: jsonable_encoder(x), session.query(Origin).all()))
session.close()
return JSONResponse(response) return JSONResponse(response)
@app.get('/-/leases') @app.get('/-/leases')
async def _leases(request: Request): async def _leases(request: Request):
response = list(map(lambda x: jsonable_encoder(x), db['lease'].all())) session = sessionmaker(bind=db)()
response = list(map(lambda x: jsonable_encoder(x), session.query(Lease).all()))
session.close()
return JSONResponse(response) return JSONResponse(response)
@ -109,15 +117,6 @@ async def client_token():
cur_time = datetime.utcnow() cur_time = datetime.utcnow()
exp_time = cur_time + relativedelta(years=12) exp_time = cur_time + relativedelta(years=12)
service_instance_public_key_configuration = {
"service_instance_public_key_me": {
"mod": hex(INSTANCE_KEY_PUB.public_key().n)[2:],
"exp": INSTANCE_KEY_PUB.public_key().e,
},
"service_instance_public_key_pem": INSTANCE_KEY_PUB.export_key().decode('utf-8'),
"key_retention_mode": "LATEST_ONLY"
}
payload = { payload = {
"jti": str(uuid4()), "jti": str(uuid4()),
"iss": "NLS Service Instance", "iss": "NLS Service Instance",
@ -129,7 +128,7 @@ async def client_token():
"scope_ref_list": [str(uuid4())], "scope_ref_list": [str(uuid4())],
"fulfillment_class_ref_list": [], "fulfillment_class_ref_list": [],
"service_instance_configuration": { "service_instance_configuration": {
"nls_service_instance_ref": "00000000-0000-0000-0000-000000000000", "nls_service_instance_ref": INSTANCE_REF,
"svc_port_set_list": [ "svc_port_set_list": [
{ {
"idx": 0, "idx": 0,
@ -139,7 +138,14 @@ async def client_token():
], ],
"node_url_list": [{"idx": 0, "url": DLS_URL, "url_qr": DLS_URL, "svc_port_set_idx": 0}] "node_url_list": [{"idx": 0, "url": DLS_URL, "url_qr": DLS_URL, "svc_port_set_idx": 0}]
}, },
"service_instance_public_key_configuration": service_instance_public_key_configuration, "service_instance_public_key_configuration": {
"service_instance_public_key_me": {
"mod": hex(INSTANCE_KEY_PUB.public_key().n)[2:],
"exp": int(INSTANCE_KEY_PUB.public_key().e),
},
"service_instance_public_key_pem": INSTANCE_KEY_PUB.export_key().decode('utf-8'),
"key_retention_mode": "LATEST_ONLY"
},
} }
content = jws.sign(payload, key=jwt_encode_key, headers=None, algorithm=ALGORITHMS.RS256) content = jws.sign(payload, key=jwt_encode_key, headers=None, algorithm=ALGORITHMS.RS256)
@ -155,21 +161,20 @@ async def client_token():
# {"candidate_origin_ref":"00112233-4455-6677-8899-aabbccddeeff","environment":{"fingerprint":{"mac_address_list":["ff:ff:ff:ff:ff:ff"]},"hostname":"my-hostname","ip_address_list":["192.168.178.123","fe80::","fe80::1%enp6s18"],"guest_driver_version":"510.85.02","os_platform":"Debian GNU/Linux 11 (bullseye) 11","os_version":"11 (bullseye)"},"registration_pending":false,"update_pending":false} # {"candidate_origin_ref":"00112233-4455-6677-8899-aabbccddeeff","environment":{"fingerprint":{"mac_address_list":["ff:ff:ff:ff:ff:ff"]},"hostname":"my-hostname","ip_address_list":["192.168.178.123","fe80::","fe80::1%enp6s18"],"guest_driver_version":"510.85.02","os_platform":"Debian GNU/Linux 11 (bullseye) 11","os_version":"11 (bullseye)"},"registration_pending":false,"update_pending":false}
@app.post('/auth/v1/origin') @app.post('/auth/v1/origin')
async def auth_v1_origin(request: Request): async def auth_v1_origin(request: Request):
j = json.loads((await request.body()).decode('utf-8')) j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow()
origin_ref = j['candidate_origin_ref'] origin_ref = j['candidate_origin_ref']
logging.info(f'> [ origin ]: {origin_ref}: {j}') logging.info(f'> [ origin ]: {origin_ref}: {j}')
data = dict( data = Origin(
origin_ref=origin_ref, origin_ref=origin_ref,
hostname=j['environment']['hostname'], hostname=j['environment']['hostname'],
guest_driver_version=j['environment']['guest_driver_version'], guest_driver_version=j['environment']['guest_driver_version'],
os_platform=j['environment']['os_platform'], os_version=j['environment']['os_version'], os_platform=j['environment']['os_platform'], os_version=j['environment']['os_version'],
) )
db['origin'].upsert(data, ['origin_ref']) Origin.create_or_update(db, data)
cur_time = datetime.utcnow()
response = { response = {
"origin_ref": origin_ref, "origin_ref": origin_ref,
"environment": j['environment'], "environment": j['environment'],
@ -188,12 +193,11 @@ async def auth_v1_origin(request: Request):
# {"code_challenge":"...","origin_ref":"00112233-4455-6677-8899-aabbccddeeff"} # {"code_challenge":"...","origin_ref":"00112233-4455-6677-8899-aabbccddeeff"}
@app.post('/auth/v1/code') @app.post('/auth/v1/code')
async def auth_v1_code(request: Request): async def auth_v1_code(request: Request):
j = json.loads((await request.body()).decode('utf-8')) j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow()
origin_ref = j['origin_ref'] origin_ref = j['origin_ref']
logging.info(f'> [ code ]: {origin_ref}: {j}') logging.info(f'> [ code ]: {origin_ref}: {j}')
cur_time = datetime.utcnow()
delta = relativedelta(minutes=15) delta = relativedelta(minutes=15)
expires = cur_time + delta expires = cur_time + delta
@ -201,16 +205,13 @@ async def auth_v1_code(request: Request):
'iat': timegm(cur_time.timetuple()), 'iat': timegm(cur_time.timetuple()),
'exp': timegm(expires.timetuple()), 'exp': timegm(expires.timetuple()),
'challenge': j['code_challenge'], 'challenge': j['code_challenge'],
'origin_ref': j['code_challenge'], 'origin_ref': j['origin_ref'],
'key_ref': SITE_KEY_XID, 'key_ref': SITE_KEY_XID,
'kid': SITE_KEY_XID 'kid': SITE_KEY_XID
} }
auth_code = jws.sign(payload, key=jwt_encode_key, headers={'kid': payload.get('kid')}, algorithm=ALGORITHMS.RS256) auth_code = jws.sign(payload, key=jwt_encode_key, headers={'kid': payload.get('kid')}, algorithm=ALGORITHMS.RS256)
db['auth'].delete(origin_ref=origin_ref, expires={'<=': cur_time - delta})
db['auth'].insert(dict(origin_ref=origin_ref, code_challenge=j['code_challenge'], expires=expires))
response = { response = {
"auth_code": auth_code, "auth_code": auth_code,
"sync_timestamp": cur_time.isoformat(), "sync_timestamp": cur_time.isoformat(),
@ -225,19 +226,16 @@ async def auth_v1_code(request: Request):
# {"auth_code":"...","code_verifier":"..."} # {"auth_code":"...","code_verifier":"..."}
@app.post('/auth/v1/token') @app.post('/auth/v1/token')
async def auth_v1_token(request: Request): async def auth_v1_token(request: Request):
j = json.loads((await request.body()).decode('utf-8')) j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow()
payload = jwt.decode(token=j['auth_code'], key=jwt_decode_key) payload = jwt.decode(token=j['auth_code'], key=jwt_decode_key)
code_challenge = payload['origin_ref'] origin_ref = payload['origin_ref']
logging.info(f'> [ auth ]: {origin_ref}: {j}')
origin_ref = db['auth'].find_one(code_challenge=code_challenge)['origin_ref']
logging.info(f'> [ auth ]: {origin_ref} ({code_challenge}): {j}')
# validate the code challenge # validate the code challenge
if payload['challenge'] != b64enc(sha256(j['code_verifier'].encode('utf-8')).digest()).rstrip(b'=').decode('utf-8'): if payload['challenge'] != b64enc(sha256(j['code_verifier'].encode('utf-8')).digest()).rstrip(b'=').decode('utf-8'):
raise HTTPException(status_code=401, detail='expected challenge did not match verifier') raise HTTPException(status_code=401, detail='expected challenge did not match verifier')
cur_time = datetime.utcnow()
access_expires_on = cur_time + TOKEN_EXPIRE_DELTA access_expires_on = cur_time + TOKEN_EXPIRE_DELTA
new_payload = { new_payload = {
@ -246,7 +244,7 @@ async def auth_v1_token(request: Request):
'iss': 'https://cls.nvidia.org', 'iss': 'https://cls.nvidia.org',
'aud': 'https://cls.nvidia.org', 'aud': 'https://cls.nvidia.org',
'exp': timegm(access_expires_on.timetuple()), 'exp': timegm(access_expires_on.timetuple()),
'origin_ref': payload['origin_ref'], 'origin_ref': origin_ref,
'key_ref': SITE_KEY_XID, 'key_ref': SITE_KEY_XID,
'kid': SITE_KEY_XID, 'kid': SITE_KEY_XID,
} }
@ -265,16 +263,12 @@ async def auth_v1_token(request: Request):
# {'fulfillment_context': {'fulfillment_class_ref_list': []}, 'lease_proposal_list': [{'license_type_qualifiers': {'count': 1}, 'product': {'name': 'NVIDIA RTX Virtual Workstation'}}], 'proposal_evaluation_mode': 'ALL_OF', 'scope_ref_list': ['00112233-4455-6677-8899-aabbccddeeff']} # {'fulfillment_context': {'fulfillment_class_ref_list': []}, 'lease_proposal_list': [{'license_type_qualifiers': {'count': 1}, 'product': {'name': 'NVIDIA RTX Virtual Workstation'}}], 'proposal_evaluation_mode': 'ALL_OF', 'scope_ref_list': ['00112233-4455-6677-8899-aabbccddeeff']}
@app.post('/leasing/v1/lessor') @app.post('/leasing/v1/lessor')
async def leasing_v1_lessor(request: Request): async def leasing_v1_lessor(request: Request):
j, token = json.loads((await request.body()).decode('utf-8')), get_token(request) j, token, cur_time = json.loads((await request.body()).decode('utf-8')), get_token(request), datetime.utcnow()
code_challenge = token['origin_ref'] origin_ref = token['origin_ref']
scope_ref_list = j['scope_ref_list'] scope_ref_list = j['scope_ref_list']
logging.info(f'> [ create ]: {origin_ref}: create leases for scope_ref_list {scope_ref_list}')
origin_ref = db['auth'].find_one(code_challenge=code_challenge)['origin_ref']
logging.info(f'> [ create ]: {origin_ref} ({code_challenge}): create leases for scope_ref_list {scope_ref_list}')
cur_time = datetime.utcnow()
lease_result_list = [] lease_result_list = []
for scope_ref in scope_ref_list: for scope_ref in scope_ref_list:
expires = cur_time + LEASE_EXPIRE_DELTA expires = cur_time + LEASE_EXPIRE_DELTA
@ -292,8 +286,8 @@ async def leasing_v1_lessor(request: Request):
} }
}) })
data = dict(origin_ref=origin_ref, lease_ref=scope_ref, lease_created=cur_time, lease_expires=expires) data = Lease(origin_ref=origin_ref, lease_ref=scope_ref, lease_created=cur_time, lease_expires=expires)
db['lease'].insert_ignore(data, ['origin_ref', 'lease_ref']) # todo: handle update Lease.create_or_update(db, data)
response = { response = {
"lease_result_list": lease_result_list, "lease_result_list": lease_result_list,
@ -309,15 +303,13 @@ async def leasing_v1_lessor(request: Request):
# venv/lib/python3.9/site-packages/nls_dal_service_instance_dls/schema/service_instance/V1_0_21__product_mapping.sql # venv/lib/python3.9/site-packages/nls_dal_service_instance_dls/schema/service_instance/V1_0_21__product_mapping.sql
@app.get('/leasing/v1/lessor/leases') @app.get('/leasing/v1/lessor/leases')
async def leasing_v1_lessor_lease(request: Request): async def leasing_v1_lessor_lease(request: Request):
token = get_token(request) token, cur_time = get_token(request), datetime.utcnow()
code_challenge = token['origin_ref'] origin_ref = token['origin_ref']
origin_ref = db['auth'].find_one(code_challenge=code_challenge)['origin_ref']
active_lease_list = list(map(lambda x: x['lease_ref'], db['lease'].find(origin_ref=origin_ref))) active_lease_list = list(map(lambda x: x['lease_ref'], db['lease'].find(origin_ref=origin_ref)))
logging.info(f'> [ leases ]: {origin_ref} ({code_challenge}): found {len(active_lease_list)} active leases') logging.info(f'> [ leases ]: {origin_ref}: found {len(active_lease_list)} active leases')
cur_time = datetime.utcnow()
response = { response = {
"active_lease_list": active_lease_list, "active_lease_list": active_lease_list,
"sync_timestamp": cur_time.isoformat(), "sync_timestamp": cur_time.isoformat(),
@ -330,17 +322,15 @@ async def leasing_v1_lessor_lease(request: Request):
# venv/lib/python3.9/site-packages/nls_core_lease/lease_single.py # venv/lib/python3.9/site-packages/nls_core_lease/lease_single.py
@app.put('/leasing/v1/lease/{lease_ref}') @app.put('/leasing/v1/lease/{lease_ref}')
async def leasing_v1_lease_renew(request: Request, lease_ref: str): async def leasing_v1_lease_renew(request: Request, lease_ref: str):
token = get_token(request) token, cur_time = get_token(request), datetime.utcnow()
code_challenge = token['origin_ref'] origin_ref = token['origin_ref']
logging.info(f'> [ renew ]: {origin_ref}: renew {lease_ref}')
origin_ref = db['auth'].find_one(code_challenge=code_challenge)['origin_ref'] entity = Lease.find_by_origin_ref_and_lease_ref(db, origin_ref, lease_ref)
logging.info(f'> [ renew ]: {origin_ref} ({code_challenge}): renew {lease_ref}') if entity is None:
if db['lease'].count(origin_ref=origin_ref, lease_ref=lease_ref) == 0:
raise HTTPException(status_code=404, detail='requested lease not available') raise HTTPException(status_code=404, detail='requested lease not available')
cur_time = datetime.utcnow()
expires = cur_time + LEASE_EXPIRE_DELTA expires = cur_time + LEASE_EXPIRE_DELTA
response = { response = {
"lease_ref": lease_ref, "lease_ref": lease_ref,
@ -351,30 +341,28 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str):
"sync_timestamp": cur_time.isoformat(), "sync_timestamp": cur_time.isoformat(),
} }
data = dict(origin_ref=origin_ref, lease_ref=lease_ref, lease_expires=expires, lease_last_update=cur_time) Lease.renew(db, entity, expires, cur_time)
db['lease'].update(data, ['origin_ref', 'lease_ref'])
return JSONResponse(response) return JSONResponse(response)
@app.delete('/leasing/v1/lessor/leases') @app.delete('/leasing/v1/lessor/leases')
async def leasing_v1_lessor_lease_remove(request: Request): async def leasing_v1_lessor_lease_remove(request: Request):
token = get_token(request) token, cur_time = get_token(request), datetime.utcnow()
code_challenge = token['origin_ref'] origin_ref = token['origin_ref']
origin_ref = db['auth'].find_one(code_challenge=code_challenge)['origin_ref'] released_lease_list = list(map(lambda x: x.lease_ref, Lease.find_by_origin_ref(db, origin_ref)))
released_lease_list = list(map(lambda x: x['lease_ref'], db['lease'].find(origin_ref=origin_ref))) deletions = Lease.ceanup(db, origin_ref)
deletions = db['lease'].delete(origin_ref=origin_ref) logging.info(f'> [ remove ]: {origin_ref}: removed {deletions} leases')
logging.info(f'> [ remove ]: {origin_ref} ({code_challenge}): removed {deletions} leases')
cur_time = datetime.utcnow()
response = { response = {
"released_lease_list": released_lease_list, "released_lease_list": released_lease_list,
"release_failure_list": None, "release_failure_list": None,
"sync_timestamp": cur_time.isoformat(), "sync_timestamp": cur_time.isoformat(),
"prompts": None "prompts": None
} }
return JSONResponse(response) return JSONResponse(response)

114
app/orm.py Normal file
View File

@ -0,0 +1,114 @@
import datetime
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, UniqueConstraint, update, and_, delete, inspect
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.future import Engine
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Origin(Base):
__tablename__ = "origin"
origin_ref = Column(CHAR(length=36), primary_key=True, unique=True, index=True) # uuid4
hostname = Column(VARCHAR(length=256), nullable=True)
guest_driver_version = Column(VARCHAR(length=10), nullable=True)
os_platform = Column(VARCHAR(length=256), nullable=True)
os_version = Column(VARCHAR(length=256), nullable=True)
def __repr__(self):
return f'Origin(origin_ref={self.origin_ref}, hostname={self.hostname})'
@staticmethod
def create_statement(engine: Engine):
from sqlalchemy.schema import CreateTable
return CreateTable(Origin.__table__).compile(engine)
@staticmethod
def create_or_update(engine: Engine, origin: "Origin"):
session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
entity = session.query(Origin).filter(Origin.origin_ref == origin.origin_ref).first()
print(entity)
if entity is None:
session.add(origin)
else:
values = dict(
hostname=origin.hostname,
guest_driver_version=origin.guest_driver_version,
os_platform=origin.os_platform,
os_version=origin.os_version,
)
session.execute(update(Origin).where(Origin.origin_ref == origin.origin_ref).values(values))
session.flush()
session.close()
class Lease(Base):
__tablename__ = "lease"
origin_ref = Column(CHAR(length=36), ForeignKey(Origin.origin_ref), primary_key=True, nullable=False, index=True) # uuid4
lease_ref = Column(CHAR(length=36), primary_key=True, nullable=False, index=True) # uuid4
lease_created = Column(DATETIME(), nullable=False)
lease_expires = Column(DATETIME(), nullable=False)
lease_updated = Column(DATETIME(), nullable=False)
def __repr__(self):
return f'Lease(origin_ref={self.origin_ref}, lease_ref={self.lease_ref}, expires={self.lease_expires})'
@staticmethod
def create_statement(engine: Engine):
from sqlalchemy.schema import CreateTable
return CreateTable(Lease.__table__).compile(engine)
@staticmethod
def create_or_update(engine: Engine, lease: "Lease"):
session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
entity = session.query(Lease).filter(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).first()
if entity is None:
session.add(lease)
else:
values = dict(lease_expires=lease.lease_expires, lease_updated=lease.lease_updated)
session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(values))
session.flush()
session.close()
@staticmethod
def find_by_origin_ref(engine: Engine, origin_ref: str) -> ["Lease"]:
session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
entities = session.query(Lease).filter(Lease.origin_ref == origin_ref).all()
session.close()
return entities
@staticmethod
def find_by_origin_ref_and_lease_ref(engine: Engine, origin_ref: str, lease_ref: str) -> "Lease":
session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
entity = session.query(Lease).filter(and_(Lease.origin_ref == origin_ref, Lease.lease_ref == lease_ref)).first()
session.close()
return entity
@staticmethod
def renew(engine: Engine, lease: "Lease", lease_expires: datetime.datetime, lease_updated: datetime.datetime):
session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
values = dict(lease_expires=lease.lease_expires, lease_updated=lease.lease_updated)
session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(values))
session.close()
@staticmethod
def cleanup(engine: Engine, origin_ref: str) -> int:
session = sessionmaker(autocommit=True, autoflush=True, bind=engine)()
deletions = session.query(Lease).delete(Lease.origin_ref == origin_ref)
session.close()
return deletions
def init(engine: Engine):
tables = [Origin, Lease]
db = inspect(engine)
session = sessionmaker(bind=engine)()
for table in tables:
if not db.dialect.has_table(engine.connect(), table.__tablename__):
session.execute(str(table.create_statement(engine)))
session.close()

View File

@ -3,6 +3,6 @@ uvicorn[standard]==0.20.0
python-jose==3.3.0 python-jose==3.3.0
pycryptodome==3.16.0 pycryptodome==3.16.0
python-dateutil==2.8.2 python-dateutil==2.8.2
dataset==1.5.2 sqlalchemy==1.4.45
markdown==3.4.1 markdown==3.4.1
python-dotenv==0.21.0 python-dotenv==0.21.0

85
test/main.py Normal file
View File

@ -0,0 +1,85 @@
from uuid import uuid4
from jose import jwt
from starlette.testclient import TestClient
import sys
# add relative path to use packages as they were in the app/ dir
sys.path.append('../')
sys.path.append('../app')
from app import main
client = TestClient(main.app)
ORIGIN_REF = str(uuid4())
def test_index():
response = client.get('/')
assert response.status_code == 200
def test_status():
response = client.get('/status')
assert response.status_code == 200
assert response.json()['status'] == 'up'
def test_client_token():
response = client.get('/client-token')
assert response.status_code == 200
def test_auth_v1_origin():
payload = {
"registration_pending": False,
"environment": {
"guest_driver_version": "guest_driver_version",
"hostname": "myhost",
"ip_address_list": ["192.168.1.123"],
"os_version": "os_version",
"os_platform": "os_platform",
"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]},
"host_driver_version": "host_driver_version"
},
"update_pending": False,
"candidate_origin_ref": ORIGIN_REF,
}
response = client.post('/auth/v1/origin', json=payload)
assert response.status_code == 200
assert response.json()['origin_ref'] == ORIGIN_REF
def test_auth_v1_code():
payload = {
"code_challenge": "0wmaiAMAlTIDyz4Fgt2/j0tXnGv72TYbbLs4ISRCZlY",
"origin_ref": ORIGIN_REF,
}
response = client.post('/auth/v1/code', json=payload)
assert response.status_code == 200
payload = jwt.get_unverified_claims(token=response.json()['auth_code'])
assert payload['origin_ref'] == ORIGIN_REF
def test_auth_v1_token():
pass
def test_leasing_v1_lessor():
pass
def test_leasing_v1_lessor_lease():
pass
def test_leasing_v1_lease_renew():
pass
def test_leasing_v1_lessor_lease_remove():
pass

View File

@ -1 +1 @@
VERSION=0.5 VERSION=0.6