|
42 | 42 | db_get_keys, db_insert_report, db_get_recent_results2, db_patch_approval2, db_get_report, |
43 | 43 | db_ensure_schema, db_get_apikeys, db_update_apikey, db_filter_apikeys, db_clear_delegates, |
44 | 44 | db_find_subjects, db_insert_result2, db_get_relevant_results2, db_add_delegate, db_get_group, |
45 | | - db_filter_accounts, |
| 45 | + db_filter_accounts, db_get_report_history, |
46 | 46 | ) |
47 | 47 |
|
48 | 48 |
|
@@ -84,6 +84,7 @@ def __init__(self): |
84 | 84 | ROLES = {'read_any': 1, 'append_any': 2, 'admin': 4, 'approve': 8} |
85 | 85 | # number of days that expired results will be considered in lieu of more recent, but unapproved ones |
86 | 86 | GRACE_PERIOD_DAYS = 7 |
| 87 | +HISTORY_LIMIT = 500 |
87 | 88 | # separator between signature and report data; use something like |
88 | 89 | # ssh-keygen \ |
89 | 90 | # -Y sign -f ~/.ssh/id_ed25519 -n report myreport.yaml |
@@ -122,7 +123,12 @@ class ViewType(Enum): |
122 | 123 | ViewType.fragment: 'scope.md', |
123 | 124 | ViewType.page: 'overview.html', |
124 | 125 | } |
125 | | -REQUIRED_TEMPLATES = tuple(set(fn for view in (VIEW_REPORT, VIEW_DETAIL, VIEW_TABLE, VIEW_SCOPE) for fn in view.values())) |
| 126 | +VIEW_HISTORY = { |
| 127 | + ViewType.markdown: 'history.md', |
| 128 | + ViewType.fragment: 'history.md', |
| 129 | + ViewType.page: 'overview.html', |
| 130 | +} |
| 131 | +REQUIRED_TEMPLATES = tuple(set(fn for view in (VIEW_REPORT, VIEW_DETAIL, VIEW_TABLE, VIEW_SCOPE, VIEW_HISTORY) for fn in view.values())) |
126 | 132 |
|
127 | 133 |
|
128 | 134 | # do I hate these globals, but I don't see another way with these frameworks |
@@ -580,8 +586,9 @@ def render_view(view, view_type, detail_page='detail', base_url='/', title=None, |
580 | 586 | stage1 = view[ViewType.fragment] |
581 | 587 | def scope_url(uuid): return f"{base_url}page/scope/{uuid}" # noqa: E306,E704 |
582 | 588 | def detail_url(subject, scope): return f"{base_url}page/{detail_page}/{subject}/{scope}" # noqa: E306,E704 |
| 589 | + def history_url(subject, scope): return f"{base_url}page/history/{subject}/{scope}" # noqa: E306,E704 |
583 | 590 | def report_url(report, *args, **kwargs): return _build_report_url(base_url, report, *args, **kwargs) # noqa: E306,E704 |
584 | | - fragment = templates_map[stage1].render(base_url=base_url, detail_url=detail_url, report_url=report_url, scope_url=scope_url, **kwargs) |
| 591 | + fragment = templates_map[stage1].render(base_url=base_url, detail_url=detail_url, history_url=history_url, report_url=report_url, scope_url=scope_url, **kwargs) |
585 | 592 | if view_type != ViewType.markdown and stage1.endswith('.md'): |
586 | 593 | fragment = markdown(fragment, extensions=['extra']) |
587 | 594 | if stage1 != stage2: |
@@ -702,6 +709,24 @@ async def get_detail_full( |
702 | 709 | ) |
703 | 710 |
|
704 | 711 |
|
| 712 | +@app.get("/{view_type}/history/{subject}/{scopeuuid}") |
| 713 | +async def get_history( |
| 714 | + request: Request, |
| 715 | + conn: Annotated[connection, Depends(get_conn)], |
| 716 | + view_type: ViewType, |
| 717 | + subject: str, |
| 718 | + scopeuuid: str, |
| 719 | +): |
| 720 | + scopes = get_scopes() |
| 721 | + scope_name = scopes[scopeuuid]['name'] if scopeuuid in scopes else scopeuuid |
| 722 | + with conn.cursor() as cur: |
| 723 | + history = db_get_report_history(cur, subject, scopeuuid, limit=HISTORY_LIMIT) |
| 724 | + return render_view( |
| 725 | + VIEW_HISTORY, view_type, history=history, subject=subject, scope_name=scope_name, |
| 726 | + history_limit=HISTORY_LIMIT, base_url=settings.base_url, title=f'📜 Report history for {subject}', |
| 727 | + ) |
| 728 | + |
| 729 | + |
705 | 730 | @app.get("/{view_type}/table") |
706 | 731 | async def get_table( |
707 | 732 | request: Request, |
@@ -774,11 +799,16 @@ async def get_results( |
774 | 799 | account: Annotated[tuple[str, str], Depends(auth)], |
775 | 800 | conn: Annotated[connection, Depends(get_conn)], |
776 | 801 | approved: Optional[bool] = None, limit: int = 10, skip: int = 0, |
| 802 | + subject: Optional[str] = None, scopeuuid: Optional[str] = None, |
777 | 803 | ): |
778 | | - """get recent results, potentially filtered by approval status""" |
779 | | - check_role(account, roles=ROLES['read_any']) |
| 804 | + """get recent results, optionally filtered by subject, scope, and approval status""" |
| 805 | + if subject is None: |
| 806 | + check_role(account, roles=ROLES['read_any']) |
| 807 | + else: |
| 808 | + check_role(account, subject, ROLES['read_any']) |
780 | 809 | with conn.cursor() as cur: |
781 | | - return db_get_recent_results2(cur, approved, limit, skip, max_age_days=GRACE_PERIOD_DAYS) |
| 810 | + return db_get_recent_results2(cur, approved, limit, skip, max_age_days=GRACE_PERIOD_DAYS, |
| 811 | + subject=subject, scopeuuid=scopeuuid) |
782 | 812 |
|
783 | 813 |
|
784 | 814 | @app.post("/results") |
|
0 commit comments