From 82876bf6b1f7471e65f61cd487492b46ac518f16 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 13:14:16 +0100 Subject: [PATCH 01/18] .gitlab-ci.yml - fixed release --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2374a20..0a0d788 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -275,9 +275,12 @@ release: when: never - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH before_script: + - set -a # make variables from "source" command available to release-cli - source version.env script: - echo "Running release-job for $VERSION" + after_script: + - set +a release: name: $CI_PROJECT_TITLE $version description: Release of $CI_PROJECT_TITLE version $VERSION From 3fad49b18ac62254eb3b0bfe23d38d0b8ccaf203 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 18:48:30 +0100 Subject: [PATCH 02/18] main.py - added api descriptions --- app/main.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index 8c30ad9..6031f26 100644 --- a/app/main.py +++ b/app/main.py @@ -69,7 +69,7 @@ async def index(): return RedirectResponse('/-/readme') -@app.get('/status', summary='* Status', description='Returns current service status, version (incl. git-commit) and some variables.', deprecated=True) +@app.get('/status', summary='* Status', description='returns current service status, version (incl. git-commit) and some variables.', deprecated=True) async def status(request: Request): return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG}) @@ -161,7 +161,7 @@ async def _lease_delete(request: Request, lease_ref: str): # venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py -@app.get('/client-token', summary='* Client-Token') +@app.get('/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance') async def client_token(): cur_time = datetime.utcnow() exp_time = cur_time + relativedelta(years=12) @@ -208,7 +208,7 @@ async def client_token(): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py # {"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', description='find or create an origin') async def auth_v1_origin(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() @@ -239,7 +239,7 @@ async def auth_v1_origin(request: Request): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py # { "environment" : { "guest_driver_version" : "guest_driver_version", "hostname" : "myhost", "ip_address_list" : [ "192.168.1.129" ], "os_version" : "os_version", "os_platform" : "os_platform", "fingerprint" : { "mac_address_list" : [ "e4:b9:7a:e5:7b:ff" ] }, "host_driver_version" : "host_driver_version" }, "origin_ref" : "00112233-4455-6677-8899-aabbccddeeff" } -@app.post('/auth/v1/origin/update') +@app.post('/auth/v1/origin/update', description='update an origin evidence') async def auth_v1_origin_update(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() @@ -267,7 +267,7 @@ async def auth_v1_origin_update(request: Request): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_core_auth/auth.py - CodeResponse # {"code_challenge":"...","origin_ref":"00112233-4455-6677-8899-aabbccddeeff"} -@app.post('/auth/v1/code') +@app.post('/auth/v1/code', description='get an authorization code') async def auth_v1_code(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() @@ -300,7 +300,7 @@ async def auth_v1_code(request: Request): # venv/lib/python3.9/site-packages/nls_services_auth/test/test_auth_controller.py # venv/lib/python3.9/site-packages/nls_core_auth/auth.py - TokenResponse # {"auth_code":"...","code_verifier":"..."} -@app.post('/auth/v1/token') +@app.post('/auth/v1/token', description='exchange auth code and verifier for token') async def auth_v1_token(request: Request): j, cur_time = json.loads((await request.body()).decode('utf-8')), datetime.utcnow() payload = jwt.decode(token=j['auth_code'], key=jwt_decode_key) @@ -337,7 +337,7 @@ 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']} -@app.post('/leasing/v1/lessor') +@app.post('/leasing/v1/lessor', description='request multiple leases (borrow) for current origin') async def leasing_v1_lessor(request: Request): j, token, cur_time = json.loads((await request.body()).decode('utf-8')), get_token(request), datetime.utcnow() @@ -377,7 +377,7 @@ async def leasing_v1_lessor(request: Request): # venv/lib/python3.9/site-packages/nls_services_lease/test/test_lease_multi_controller.py # 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', description='get active leases for current origin') async def leasing_v1_lessor_lease(request: Request): token, cur_time = get_token(request), datetime.utcnow() @@ -396,7 +396,7 @@ async def leasing_v1_lessor_lease(request: Request): # 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}', description='renew a lease') async def leasing_v1_lease_renew(request: Request, lease_ref: str): token, cur_time = get_token(request), datetime.utcnow() @@ -422,7 +422,7 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str): return JSONResponse(response) -@app.delete('/leasing/v1/lessor/leases') +@app.delete('/leasing/v1/lessor/leases', description='release all leases') async def leasing_v1_lessor_lease_remove(request: Request): token, cur_time = get_token(request), datetime.utcnow() From e0843ca1d4960e8c342c19f511a36c8a479f9559 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 18:59:26 +0100 Subject: [PATCH 03/18] code styling --- app/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 6031f26..3c8d7da 100644 --- a/app/main.py +++ b/app/main.py @@ -341,7 +341,7 @@ async def auth_v1_token(request: Request): async def leasing_v1_lessor(request: Request): j, token, cur_time = json.loads((await request.body()).decode('utf-8')), get_token(request), datetime.utcnow() - origin_ref = token['origin_ref'] + origin_ref = token.get('origin_ref') scope_ref_list = j['scope_ref_list'] logging.info(f'> [ create ]: {origin_ref}: create leases for scope_ref_list {scope_ref_list}') @@ -381,7 +381,7 @@ async def leasing_v1_lessor(request: Request): async def leasing_v1_lessor_lease(request: Request): token, cur_time = get_token(request), datetime.utcnow() - origin_ref = token['origin_ref'] + origin_ref = token.get('origin_ref') active_lease_list = list(map(lambda x: x.lease_ref, Lease.find_by_origin_ref(db, origin_ref))) logging.info(f'> [ leases ]: {origin_ref}: found {len(active_lease_list)} active leases') @@ -400,7 +400,7 @@ async def leasing_v1_lessor_lease(request: Request): async def leasing_v1_lease_renew(request: Request, lease_ref: str): token, cur_time = get_token(request), datetime.utcnow() - origin_ref = token['origin_ref'] + origin_ref = token.get('origin_ref') logging.info(f'> [ renew ]: {origin_ref}: renew {lease_ref}') entity = Lease.find_by_origin_ref_and_lease_ref(db, origin_ref, lease_ref) @@ -426,7 +426,7 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str): async def leasing_v1_lessor_lease_remove(request: Request): token, cur_time = get_token(request), datetime.utcnow() - origin_ref = token['origin_ref'] + origin_ref = token.get('origin_ref') released_lease_list = list(map(lambda x: x.lease_ref, Lease.find_by_origin_ref(db, origin_ref))) deletions = Lease.cleanup(db, origin_ref) From 569ca8b3eaa9e679adc8e317f557c4bee8ad5f9f Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 19:00:14 +0100 Subject: [PATCH 04/18] orm.py - fixed renewing timestamps from params --- app/orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/orm.py b/app/orm.py index 62a9fb1..4cd1461 100644 --- a/app/orm.py +++ b/app/orm.py @@ -125,7 +125,7 @@ class Lease(Base): @staticmethod def renew(engine: Engine, lease: "Lease", lease_expires: datetime.datetime, lease_updated: datetime.datetime): session = sessionmaker(bind=engine)() - x = dict(lease_expires=lease.lease_expires, lease_updated=lease.lease_updated) + x = dict(lease_expires=lease_expires, lease_updated=lease_updated) session.execute(update(Lease).where(and_(Lease.origin_ref == lease.origin_ref, Lease.lease_ref == lease.lease_ref)).values(**x)) session.commit() session.close() From 17978c2e2eb26e303c345fd5a7c9ea7eca2a82d7 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 19:03:09 +0100 Subject: [PATCH 05/18] main.py - added endpoint to release single lease --- app/main.py | 25 +++++++++++++++++++++++++ app/orm.py | 7 +++++++ test/main.py | 11 +++++++++++ 3 files changed, 43 insertions(+) diff --git a/app/main.py b/app/main.py index 3c8d7da..cc7a905 100644 --- a/app/main.py +++ b/app/main.py @@ -422,6 +422,31 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str): return JSONResponse(response) +@app.delete('/leasing/v1/lease/{lease_ref}', description='release (return) a lease') +async def leasing_v1_lease_delete(request: Request, lease_ref: str): + token, cur_time = get_token(request), datetime.utcnow() + + origin_ref = token.get('origin_ref') + logging.info(f'> [ return ]: {origin_ref}: return {lease_ref}') + + entity = Lease.find_by_lease_ref(db, lease_ref) + if entity.origin_ref != origin_ref: + raise HTTPException(status_code=403, detail='access or operation forbidden') + if entity is None: + raise HTTPException(status_code=404, detail='requested lease not available') + + if Lease.delete(db, lease_ref) == 0: + raise HTTPException(status_code=404, detail='lease not found') + + response = { + "lease_ref": lease_ref, + "prompts": None, + "sync_timestamp": cur_time.isoformat(), + } + + return JSONResponse(response) + + @app.delete('/leasing/v1/lessor/leases', description='release all leases') async def leasing_v1_lessor_lease_remove(request: Request): token, cur_time = get_token(request), datetime.utcnow() diff --git a/app/orm.py b/app/orm.py index 4cd1461..0f5d386 100644 --- a/app/orm.py +++ b/app/orm.py @@ -115,6 +115,13 @@ class Lease(Base): session.close() return entities + @staticmethod + def find_by_lease_ref(engine: Engine, lease_ref: str) -> "Lease": + session = sessionmaker(bind=engine)() + entity = session.query(Lease).filter(Lease.lease_ref == lease_ref).first() + session.close() + return entity + @staticmethod def find_by_origin_ref_and_lease_ref(engine: Engine, origin_ref: str, lease_ref: str) -> "Lease": session = sessionmaker(bind=engine)() diff --git a/test/main.py b/test/main.py index 364c5b0..6a69244 100644 --- a/test/main.py +++ b/test/main.py @@ -205,7 +205,18 @@ def test_leasing_v1_lease_renew(): assert response.json()['lease_ref'] == LEASE_REF +def test_leasing_v1_lease_delete(): + bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) + bearer_token = f'Bearer {bearer_token}' + response = client.delete(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': bearer_token}) + assert response.status_code == 200 + + assert response.json()['lease_ref'] == LEASE_REF + + def test_leasing_v1_lessor_lease_remove(): + test_leasing_v1_lessor() + bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) bearer_token = f'Bearer {bearer_token}' response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': bearer_token}) From 9a5cf9ff81dbe0f67a14e199cf38d1a5a222f707 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 19:07:30 +0100 Subject: [PATCH 06/18] code styling --- test/main.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/test/main.py b/test/main.py index 6a69244..abdeacf 100644 --- a/test/main.py +++ b/test/main.py @@ -33,6 +33,12 @@ jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), al jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) +def __bearer_token(origin_ref: str) -> str: + token = jwt.encode({"origin_ref": origin_ref}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) + token = f'Bearer {token}' + return token + + def test_index(): response = client.get('/') assert response.status_code == 200 @@ -175,9 +181,7 @@ def test_leasing_v1_lessor(): 'scope_ref_list': [LEASE_REF] } - bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) - bearer_token = f'Bearer {bearer_token}' - response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': bearer_token}) + response = client.post('/leasing/v1/lessor', json=payload, headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 lease_result_list = response.json()['lease_result_list'] @@ -186,9 +190,7 @@ def test_leasing_v1_lessor(): def test_leasing_v1_lessor_lease(): - bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) - bearer_token = f'Bearer {bearer_token}' - response = client.get('/leasing/v1/lessor/leases', headers={'authorization': bearer_token}) + response = client.get('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 active_lease_list = response.json()['active_lease_list'] @@ -197,18 +199,14 @@ def test_leasing_v1_lessor_lease(): def test_leasing_v1_lease_renew(): - bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) - bearer_token = f'Bearer {bearer_token}' - response = client.put(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': bearer_token}) + response = client.put(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 assert response.json()['lease_ref'] == LEASE_REF def test_leasing_v1_lease_delete(): - bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) - bearer_token = f'Bearer {bearer_token}' - response = client.delete(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': bearer_token}) + response = client.delete(f'/leasing/v1/lease/{LEASE_REF}', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 assert response.json()['lease_ref'] == LEASE_REF @@ -216,10 +214,8 @@ def test_leasing_v1_lease_delete(): def test_leasing_v1_lessor_lease_remove(): test_leasing_v1_lessor() - - bearer_token = jwt.encode({"origin_ref": ORIGIN_REF}, key=jwt_encode_key, algorithm=ALGORITHMS.RS256) - bearer_token = f'Bearer {bearer_token}' - response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': bearer_token}) + + response = client.delete('/leasing/v1/lessor/leases', headers={'authorization': __bearer_token(ORIGIN_REF)}) assert response.status_code == 200 released_lease_list = response.json()['released_lease_list'] From a8504f301791ecad5c6003625965ff7ef59f8868 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 19:14:49 +0100 Subject: [PATCH 07/18] hardcoded default CORS to https, since drivers only support secure connections --- README.md | 4 +++- app/main.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1c205ad..8db0824 100644 --- a/README.md +++ b/README.md @@ -287,12 +287,14 @@ After first success you have to replace `--issue` with `--renew`. | `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable | | `LEASE_EXPIRE_DAYS` | `90` | Lease time in days | | `DATABASE` | `sqlite:///db.sqlite` | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) | -| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) | +| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \* | | `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | | `INSTANCE_REF` | `00000000-0000-0000-0000-000000000000` | Instance identification uuid | | `INSTANCE_KEY_RSA` | `/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs | | `INSTANCE_KEY_PUB` | `/cert/instance.public.pem` | Site-wide public key | +\* Always use `https`, since guest-drivers only support secure connections! + # Setup (Client) **The token file has to be copied! It's not enough to C&P file contents, because there can be special characters.** diff --git a/app/main.py b/app/main.py index cc7a905..a9230f1 100644 --- a/app/main.py +++ b/app/main.py @@ -40,8 +40,7 @@ INSTANCE_KEY_RSA = load_key(str(env('INSTANCE_KEY_RSA', join(dirname(__file__), INSTANCE_KEY_PUB = load_key(str(env('INSTANCE_KEY_PUB', join(dirname(__file__), 'cert/instance.public.pem')))) TOKEN_EXPIRE_DELTA = relativedelta(hours=1) # days=1 LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90))) - -CORS_ORIGINS = env('CORS_ORIGINS').split(',') if (env('CORS_ORIGINS')) else f'https://{DLS_URL}' # todo: prevent static https +CORS_ORIGINS = env('CORS_ORIGINS').split(',') if (env('CORS_ORIGINS')) else f'https://{DLS_URL}' jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) From c0ab3a589fe089ea0e4a5afbaeb7ba5145c60725 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 20:33:50 +0100 Subject: [PATCH 08/18] migrated '/client-token' to '/-/client-token' --- app/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index a9230f1..f1394b8 100644 --- a/app/main.py +++ b/app/main.py @@ -160,8 +160,8 @@ async def _lease_delete(request: Request, lease_ref: str): # venv/lib/python3.9/site-packages/nls_core_service_instance/service_instance_token_manager.py -@app.get('/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance') -async def client_token(): +@app.get('/-/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance') +async def _client_token(): cur_time = datetime.utcnow() exp_time = cur_time + relativedelta(years=12) @@ -205,6 +205,11 @@ async def client_token(): return response +@app.get('/client-token', summary='* Client-Token', description='creates a new messenger token for this service instance', deprecated=True) +async def client_token(): + return RedirectResponse('/-/client-token') + + # venv/lib/python3.9/site-packages/nls_services_auth/test/test_origins_controller.py # {"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', description='find or create an origin') From fa3a06a3605610e2b26269e45ec595b8023ca738 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 20:40:42 +0100 Subject: [PATCH 09/18] code styling --- app/main.py | 6 +++--- app/util.py | 29 ++++++++++++++++++----------- test/main.py | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/main.py b/app/main.py index f1394b8..a29fd13 100644 --- a/app/main.py +++ b/app/main.py @@ -50,15 +50,15 @@ app.add_middleware( CORSMiddleware, allow_origins=CORS_ORIGINS, allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], + allow_methods=['*'], + allow_headers=['*'], ) logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) def get_token(request: Request) -> dict: - authorization_header = request.headers['authorization'] + authorization_header = request.headers.get('authorization') token = authorization_header.split(' ')[1] return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False}) diff --git a/app/util.py b/app/util.py index 2de1a57..ca3eec6 100644 --- a/app/util.py +++ b/app/util.py @@ -1,21 +1,28 @@ -try: - # Crypto | Cryptodome on Debian - from Crypto.PublicKey import RSA - from Crypto.PublicKey.RSA import RsaKey -except ModuleNotFoundError: - from Cryptodome.PublicKey import RSA - from Cryptodome.PublicKey.RSA import RsaKey - - def load_file(filename) -> bytes: with open(filename, 'rb') as file: content = file.read() return content -def load_key(filename) -> RsaKey: +def load_key(filename) -> "RsaKey": + try: + # Crypto | Cryptodome on Debian + from Crypto.PublicKey import RSA + from Crypto.PublicKey.RSA import RsaKey + except ModuleNotFoundError: + from Cryptodome.PublicKey import RSA + from Cryptodome.PublicKey.RSA import RsaKey + return RSA.import_key(extern_key=load_file(filename), passphrase=None) -def generate_key() -> RsaKey: +def generate_key() -> "RsaKey": + try: + # Crypto | Cryptodome on Debian + from Crypto.PublicKey import RSA + from Crypto.PublicKey.RSA import RsaKey + except ModuleNotFoundError: + from Cryptodome.PublicKey import RSA + from Cryptodome.PublicKey.RSA import RsaKey + return RSA.generate(bits=2048) diff --git a/test/main.py b/test/main.py index abdeacf..50f242b 100644 --- a/test/main.py +++ b/test/main.py @@ -16,7 +16,7 @@ sys.path.append('../') sys.path.append('../app') from app import main -from app.util import generate_key, load_key +from app.util import load_key client = TestClient(main.app) From 400c983025d359f8b89227462065b7fa925e8147 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 20:41:02 +0100 Subject: [PATCH 10/18] added redirect for "/-/" route --- app/main.py | 7 ++++++- test/main.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index a29fd13..3f0b35f 100644 --- a/app/main.py +++ b/app/main.py @@ -63,7 +63,7 @@ def get_token(request: Request) -> dict: return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False}) -@app.get('/', summary='* Index') +@app.get('/', summary='Index') async def index(): return RedirectResponse('/-/readme') @@ -73,6 +73,11 @@ async def status(request: Request): return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG}) +@app.get('/-/', summary='* Index') +async def _index(): + return RedirectResponse('/-/readme') + + @app.get('/-/health', summary='* Health') async def _health(request: Request): return JSONResponse({'status': 'up', 'version': VERSION, 'commit': COMMIT, 'debug': DEBUG}) diff --git a/test/main.py b/test/main.py index 50f242b..9ef4457 100644 --- a/test/main.py +++ b/test/main.py @@ -67,6 +67,11 @@ def test_manage(): def test_client_token(): + response = client.get('/-/client-token') + assert response.status_code == 200 + + +def test_client_token_deprecated(): response = client.get('/client-token') assert response.status_code == 200 From 1f56d31351761db0bf71bad3e21735d0086ef045 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Thu, 29 Dec 2022 20:42:40 +0100 Subject: [PATCH 11/18] code styling --- app/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index 3f0b35f..c13e81b 100644 --- a/app/main.py +++ b/app/main.py @@ -57,7 +57,7 @@ app.add_middleware( logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) -def get_token(request: Request) -> dict: +def __get_token(request: Request) -> dict: authorization_header = request.headers.get('authorization') token = authorization_header.split(' ')[1] return jwt.decode(token=token, key=jwt_decode_key, algorithms=ALGORITHMS.RS256, options={'verify_aud': False}) @@ -348,7 +348,7 @@ 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']} @app.post('/leasing/v1/lessor', description='request multiple leases (borrow) for current origin') async def leasing_v1_lessor(request: Request): - j, token, cur_time = json.loads((await request.body()).decode('utf-8')), get_token(request), datetime.utcnow() + j, token, cur_time = json.loads((await request.body()).decode('utf-8')), __get_token(request), datetime.utcnow() origin_ref = token.get('origin_ref') scope_ref_list = j['scope_ref_list'] @@ -388,7 +388,7 @@ 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 @app.get('/leasing/v1/lessor/leases', description='get active leases for current origin') async def leasing_v1_lessor_lease(request: Request): - token, cur_time = get_token(request), datetime.utcnow() + token, cur_time = __get_token(request), datetime.utcnow() origin_ref = token.get('origin_ref') @@ -407,7 +407,7 @@ async def leasing_v1_lessor_lease(request: Request): # venv/lib/python3.9/site-packages/nls_core_lease/lease_single.py @app.put('/leasing/v1/lease/{lease_ref}', description='renew a lease') async def leasing_v1_lease_renew(request: Request, lease_ref: str): - token, cur_time = get_token(request), datetime.utcnow() + token, cur_time = __get_token(request), datetime.utcnow() origin_ref = token.get('origin_ref') logging.info(f'> [ renew ]: {origin_ref}: renew {lease_ref}') @@ -433,7 +433,7 @@ async def leasing_v1_lease_renew(request: Request, lease_ref: str): @app.delete('/leasing/v1/lease/{lease_ref}', description='release (return) a lease') async def leasing_v1_lease_delete(request: Request, lease_ref: str): - token, cur_time = get_token(request), datetime.utcnow() + token, cur_time = __get_token(request), datetime.utcnow() origin_ref = token.get('origin_ref') logging.info(f'> [ return ]: {origin_ref}: return {lease_ref}') @@ -458,7 +458,7 @@ async def leasing_v1_lease_delete(request: Request, lease_ref: str): @app.delete('/leasing/v1/lessor/leases', description='release all leases') async def leasing_v1_lessor_lease_remove(request: Request): - token, cur_time = get_token(request), datetime.utcnow() + token, cur_time = __get_token(request), datetime.utcnow() origin_ref = token.get('origin_ref') From 3d83e533da831e787720c1e58b4e58124325d578 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 30 Dec 2022 03:50:48 +0100 Subject: [PATCH 12/18] fixed client-token filename (missing .tok extension) --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index c13e81b..b6e4b85 100644 --- a/app/main.py +++ b/app/main.py @@ -204,7 +204,7 @@ async def _client_token(): content = jws.sign(payload, key=jwt_encode_key, headers=None, algorithm=ALGORITHMS.RS256) response = StreamingResponse(iter([content]), media_type="text/plain") - filename = f'client_configuration_token_{datetime.now().strftime("%d-%m-%y-%H-%M-%S")}' + filename = f'client_configuration_token_{datetime.now().strftime("%d-%m-%y-%H-%M-%S")}.tok' response.headers["Content-Disposition"] = f'attachment; filename={filename}' return response From 478ca0ab63430b4d3d7945195c137d5a61cedd7f Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 30 Dec 2022 07:02:50 +0100 Subject: [PATCH 13/18] added some comments --- .DEBIAN/postinst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.DEBIAN/postinst b/.DEBIAN/postinst index f0d9bdc..5680ac1 100644 --- a/.DEBIAN/postinst +++ b/.DEBIAN/postinst @@ -6,6 +6,7 @@ CONFIG_DIR=/etc/fastapi-dls echo "> Create config directory ..." mkdir -p $CONFIG_DIR +# normally we would define services in `conffiles` and as separate file, but we like to keep thinks simple. echo "> Install service ..." cat </etc/systemd/system/fastapi-dls.service [Unit] @@ -37,6 +38,7 @@ EOF systemctl daemon-reload +# normally we would define configfiles in `conffiles` and as separate file, but we like to keep thinks simple. if [[ ! -f $CONFIG_DIR/env ]]; then echo "> Writing initial config ..." touch $CONFIG_DIR/env From b0e10004f105bf33567228f3e2b73187e1450f4e Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 30 Dec 2022 07:11:02 +0100 Subject: [PATCH 14/18] README.md - added windows license key installation from powershell --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 8db0824..8a16f54 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,14 @@ nvidia-smi -q | grep "License" Download file and place it into `C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken`. Now restart `NvContainerLocalSystem` service. +**Power-Shell** + +```Shell +curl.exe --insecure -X GET https:///client-token -o "C:\Program Files\NVIDIA Corporation\vGPU Licensing\ClientConfigToken\client_configuration_token_$($(Get-Date).tostring('dd-MM-yy-hh-mm-ss')).tok" +Restart-Service NVDisplay.ContainerLocalSystem +'C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe' -q | Select-String "License" +``` + # Troubleshoot ## Linux From 5fc9fc8e0ac35f58e8c4667d623e347fd56abcd8 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 30 Dec 2022 07:14:25 +0100 Subject: [PATCH 15/18] added documentation to debian service --- .DEBIAN/postinst | 1 + 1 file changed, 1 insertion(+) diff --git a/.DEBIAN/postinst b/.DEBIAN/postinst index 5680ac1..d4ceee0 100644 --- a/.DEBIAN/postinst +++ b/.DEBIAN/postinst @@ -11,6 +11,7 @@ echo "> Install service ..." cat </etc/systemd/system/fastapi-dls.service [Unit] Description=Service for fastapi-dls +Documentation=https://git.collinwebdesigns.de/oscar.krause/fastapi-dls After=network.target [Service] From 5e40d7944a671f154f923d614d3072374b88f560 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 30 Dec 2022 07:33:17 +0100 Subject: [PATCH 16/18] PKGBUILD - updated service running uvicorn natively instead of calling main.py - fixed issue with not loading env variables inside to fastapi - fixed to not using "python main.py" which meant for development --- .PKGBUILD/PKGBUILD | 13 ++++++++----- .PKGBUILD/fastapi-dls.default | 5 +++++ .PKGBUILD/fastapi-dls.service | 11 ++++++----- .PKGBUILD/fastapi-dls.tmpfiles | 2 ++ 4 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 .PKGBUILD/fastapi-dls.tmpfiles diff --git a/.PKGBUILD/PKGBUILD b/.PKGBUILD/PKGBUILD index f8fb48e..97d3995 100644 --- a/.PKGBUILD/PKGBUILD +++ b/.PKGBUILD/PKGBUILD @@ -1,8 +1,8 @@ -# Maintainer: samicrusader # Maintainer: Oscar Krause +# Contributor: samicrusader pkgname=fastapi-dls -pkgver=0.0 +pkgver=1.1 pkgrel=1 pkgdesc='NVIDIA DLS server implementation with FastAPI' arch=('any') @@ -13,10 +13,12 @@ provider=("$pkgname") install="$pkgname.install" source=('git+file:///builds/oscar.krause/fastapi-dls' # https://gitea.publichub.eu/oscar.krause/fastapi-dls.git "$pkgname.default" - "$pkgname.service") + "$pkgname.service" + "$pkgname.tmpfiles") sha256sums=('SKIP' - '4c07e9b627853bd4f3a398371912fc72302dac33f43e4cb7e9b79746cc9c9136' - '10cb98d64f8bf37b11a60510793c187cc664e63c895d1205781c21fa2e703f32') + 'fbd015449a30c0ae82733289a56eb98151dcfab66c91b37fe8e202e39f7a5edb' + '2719338541104c537453a65261c012dda58e1dbee99154cf4f33b526ee6ca22e' + '3dc60140c08122a8ec0e7fa7f0937eb8c1288058890ba09478420fc30ce9e30c') pkgver() { source $srcdir/$pkgname/version.env @@ -46,4 +48,5 @@ package() { install -Dm755 "$srcdir/$pkgname/app/util.py" "$pkgdir/opt/$pkgname/util.py" install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname" install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service" + install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf" } diff --git a/.PKGBUILD/fastapi-dls.default b/.PKGBUILD/fastapi-dls.default index 0add216..0bfb664 100644 --- a/.PKGBUILD/fastapi-dls.default +++ b/.PKGBUILD/fastapi-dls.default @@ -3,6 +3,7 @@ DEBUG=false # Where the client can find the DLS server ## DLS_URL should be a hostname +LISTEN_IP="0.0.0.0" DLS_URL="localhost.localdomain" DLS_PORT=8443 CORS_ORIGINS="https://$DLS_URL:$DLS_PORT" @@ -21,3 +22,7 @@ INSTANCE_REF="<>" # Site-wide signing keys INSTANCE_KEY_RSA="/var/lib/fastapi-dls/instance.private.pem" INSTANCE_KEY_PUB="/var/lib/fastapi-dls/instance.public.pem" + +# TLS certificate +INSTANCE_SSL_CERT="/var/lib/fastapi-dls/cert/webserver.crt" +INSTANCE_SSL_KEY="/var/lib/fastapi-dls/cert/webserver.key" diff --git a/.PKGBUILD/fastapi-dls.service b/.PKGBUILD/fastapi-dls.service index 1bca7b7..8670ab5 100644 --- a/.PKGBUILD/fastapi-dls.service +++ b/.PKGBUILD/fastapi-dls.service @@ -4,12 +4,13 @@ Documentation=https://git.collinwebdesigns.de/oscar.krause/fastapi-dls After=network.target [Service] -Type=forking +Type=simple +AmbientCapabilities=CAP_NET_BIND_SERVICE EnvironmentFile=/etc/default/fastapi-dls -ExecStart=/usr/bin/python /opt/fastapi-dls/main.py -WorkingDir=/opt/fastapi-dls +ExecStart=/usr/bin/uvicorn main:app --proxy-headers --env-file=/etc/default/fastapi-dls --host=${LISTEN_IP} --port=${DLS_PORT} --app-dir=/opt/fastapi-dls --ssl-keyfile=${INSTANCE_SSL_KEY} --ssl-certfile=${INSTANCE_SSL_CERT} Restart=on-abort -User=root +User=http +Group=http [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target \ No newline at end of file diff --git a/.PKGBUILD/fastapi-dls.tmpfiles b/.PKGBUILD/fastapi-dls.tmpfiles new file mode 100644 index 0000000..a9dc351 --- /dev/null +++ b/.PKGBUILD/fastapi-dls.tmpfiles @@ -0,0 +1,2 @@ +d /var/lib/fastapi-dls 0755 http http +d /var/lib/fastapi-dls/cert 0755 http http From 2368cc2578683921bf25c797fe96b694893641f1 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 30 Dec 2022 07:37:36 +0100 Subject: [PATCH 17/18] bump version to 1.2 --- version.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.env b/version.env index c350bf1..955cc3f 100644 --- a/version.env +++ b/version.env @@ -1 +1 @@ -VERSION=1.1 +VERSION=1.2 From 98d7492534736f36d68a37d76685246b36b08e1c Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Fri, 30 Dec 2022 07:42:57 +0100 Subject: [PATCH 18/18] main.py - fixed cors parsing --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index b6e4b85..63df4f4 100644 --- a/app/main.py +++ b/app/main.py @@ -40,7 +40,7 @@ INSTANCE_KEY_RSA = load_key(str(env('INSTANCE_KEY_RSA', join(dirname(__file__), INSTANCE_KEY_PUB = load_key(str(env('INSTANCE_KEY_PUB', join(dirname(__file__), 'cert/instance.public.pem')))) TOKEN_EXPIRE_DELTA = relativedelta(hours=1) # days=1 LEASE_EXPIRE_DELTA = relativedelta(days=int(env('LEASE_EXPIRE_DAYS', 90))) -CORS_ORIGINS = env('CORS_ORIGINS').split(',') if (env('CORS_ORIGINS')) else f'https://{DLS_URL}' +CORS_ORIGINS = str(env('CORS_ORIGINS', '')).split(',') if (env('CORS_ORIGINS')) else [f'https://{DLS_URL}'] jwt_encode_key = jwk.construct(INSTANCE_KEY_RSA.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256) jwt_decode_key = jwk.construct(INSTANCE_KEY_PUB.export_key().decode('utf-8'), algorithm=ALGORITHMS.RS256)