Skip to content

Commit c017e51

Browse files
committed
Store compliance results in db, output changes
Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent 3d109ea commit c017e51

2 files changed

Lines changed: 91 additions & 6 deletions

File tree

compliance-monitor/monitor.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
db_get_keys, db_insert_report, db_get_recent_results2, db_patch_approval2, db_get_report,
4343
db_ensure_schema, db_get_apikeys, db_update_apikey, db_filter_apikeys, db_clear_delegates,
4444
db_find_subjects, db_insert_result2, db_get_relevant_results2, db_add_delegate, db_get_group,
45+
db_get_relevant_compliance_results, db_insert_compliance_result,
4546
)
4647

4748

@@ -463,11 +464,12 @@ async def post_report(
463464
if not documents:
464465
raise HTTPException(status_code=200, detail="empty reports")
465466

466-
allowed_subjects = {auth_subject} | set(delegation_subjects)
467-
for document in documents:
468-
check_role(account, document['subject'], ROLES['append_any'])
469-
if document['subject'] not in allowed_subjects:
470-
raise HTTPException(status_code=401, detail="delegation problem?")
467+
reported_subjects = {document['subject'] for document in documents}
468+
for subj in reported_subjects:
469+
check_role(account, subj, ROLES['append_any'])
470+
extra_subjects = reported_subjects - {auth_subject} - set(delegation_subjects)
471+
if extra_subjects:
472+
raise HTTPException(status_code=401, detail="delegation problem?")
471473

472474
with conn.cursor() as cur:
473475
for document, json_text in zip(documents, json_texts):
@@ -495,6 +497,32 @@ async def post_report(
495497
db_insert_result2(cur, checked_at, subject, scopeuuid, version, check, result, approval, reportid)
496498
conn.commit()
497499

500+
checked_at = datetime.now()
501+
for approved_only in (False, True):
502+
with conn.cursor() as cur:
503+
# fetch latest compliance results before new report
504+
rows = db_get_relevant_compliance_results(cur, approved_only=approved_only)
505+
results0 = defaultdict(lambda: defaultdict(dict))
506+
for row in rows:
507+
subj, scope_uuid, version, result, _, _ = row
508+
if subj not in reported_subjects:
509+
continue
510+
results0[subj][scope_uuid][version] = result
511+
# compute latest compliance results after new report
512+
rows2 = db_get_relevant_results2(cur, approved_only=approved_only)
513+
results = convert_result_rows_to_dict2(rows2, get_scopes())
514+
# update compliance table and report changes
515+
for subj, subj_results in results.items():
516+
if subj not in reported_subjects:
517+
continue
518+
for scope_uuid, scope_results in subj_results.items():
519+
for version, version_results in scope_results['versions'].items():
520+
result = results0[subj][scope_uuid].get(version, 0)
521+
new_result = version_results['result']
522+
db_insert_compliance_result(cur, checked_at, subj, scope_uuid, version, new_result, approval)
523+
if new_result != result:
524+
print(f"{subj} {scope_uuid} {version}: {result} -> {new_result}")
525+
498526

499527
def convert_result_rows_to_dict2(
500528
rows, scopes_lookup, grace_period_days=0, scopes=(), subjects=(), include_report=False, include_drafts=False,

compliance-monitor/sql.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# list schema versions in ascending order
55
SCHEMA_VERSION_KEY = 'version'
6-
SCHEMA_VERSIONS = ['v1', 'v2', 'v3', 'v4']
6+
SCHEMA_VERSIONS = ['v1', 'v2', 'v3', 'v4', 'v5']
77
# use ... (Ellipsis) here to indicate that no default value exists (will lead to error if no value is given)
88
ACCOUNT_DEFAULTS = {'subject': ..., 'api_key': ..., 'roles': ..., 'group': None}
99
PUBLIC_KEY_DEFAULTS = {'public_key': ..., 'public_key_type': ..., 'public_key_name': ...}
@@ -143,6 +143,23 @@ def db_ensure_schema_v4(cur: cursor):
143143
''')
144144

145145

146+
def db_ensure_schema_v5(cur: cursor):
147+
# v5 mainly extends v4
148+
db_ensure_schema_v4(cur)
149+
# introduce tables compliance that track compliance over time
150+
cur.execute('''
151+
CREATE TABLE IF NOT EXISTS compliance (
152+
resultid SERIAL PRIMARY KEY,
153+
checked_at timestamp NOT NULL,
154+
subject text NOT NULL,
155+
scopeuuid text NOT NULL,
156+
version text NOT NULL,
157+
result int,
158+
approval boolean
159+
);
160+
''')
161+
162+
146163
def db_upgrade_data_v1_v2(cur):
147164
# we are going to drop table result, but use delete anyway to have the transaction safety
148165
cur.execute('''
@@ -219,6 +236,10 @@ def db_upgrade_schema(conn: connection, cur: cursor):
219236
db_ensure_schema_v4(cur)
220237
db_set_schema_version(cur, 'v4')
221238
conn.commit()
239+
elif current == 'v4':
240+
db_ensure_schema_v5(cur)
241+
db_set_schema_version(cur, 'v5')
242+
conn.commit()
222243

223244

224245
def db_ensure_schema(conn: connection):
@@ -424,3 +445,39 @@ def db_patch_approval2(cur: cursor, record):
424445
RETURNING resultid;''', record)
425446
resultid, = cur.fetchone()
426447
return resultid
448+
449+
450+
def db_insert_compliance_result(
451+
cur: cursor, checked_at, subject, scopeuuid, version, result, approval
452+
):
453+
# this is an exception in that we don't use a record parameter (it's just not as practical here)
454+
cur.execute('''
455+
INSERT INTO compliance (checked_at, subject, scopeuuid, version, result, approval)
456+
VALUES (%s, %s, %s, %s, %s, %s)
457+
RETURNING resultid;''', (checked_at, subject, scopeuuid, version, result, approval))
458+
resultid, = cur.fetchone()
459+
return resultid
460+
461+
462+
def db_get_relevant_compliance_results(
463+
cur: cursor,
464+
subject=None, scopeuuid=None, version=None, approved_only=True,
465+
):
466+
"""for each combination of scope/version/check, get the most recent test result that is still valid"""
467+
# find the latest result per subject/scopeuuid/version/checkid for this subject
468+
# DISTINCT ON is a Postgres-specific construct that comes in very handy here :)
469+
cur.execute(sql.SQL('''
470+
SELECT DISTINCT ON (subject, scopeuuid, version)
471+
subject, scopeuuid, version, result, approval, checked_at
472+
FROM compliance
473+
{filter_condition}
474+
ORDER BY subject, scopeuuid, version, checked_at DESC;
475+
''').format(
476+
filter_condition=make_where_clause(
477+
sql.SQL('approval') if approved_only else None,
478+
None if scopeuuid is None else sql.SQL('scopeuuid = %(scopeuuid)s'),
479+
None if version is None else sql.SQL('version = %(version)s'),
480+
None if subject is None else sql.SQL('subject = %(subject)s'),
481+
),
482+
), {"subject": subject, "scopeuuid": scopeuuid, "version": version})
483+
return cur.fetchall()

0 commit comments

Comments
 (0)