Skip to content

Commit 04e712a

Browse files
authored
Merge pull request #545 from crocs-muni/copilot/add-accounting-logs-page
Add accounting logs page to admin interface
2 parents 4bbd279 + b021971 commit 04e712a

File tree

5 files changed

+192
-0
lines changed

5 files changed

+192
-0
lines changed

sec_certs_page/admin/views.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,58 @@ def edit_user(username):
266266
# prepopulate form
267267
form.roles.data = user_doc.get("roles", [])
268268
return render_template("admin/users/edit.html.jinja2", user=user_doc, form=form)
269+
270+
271+
@admin.route("/accounting")
272+
@login_required
273+
@admin_permission.require()
274+
@register_breadcrumb(admin, ".accounting", "Accounting")
275+
def accounting():
276+
page = int(request.args.get("page", 1))
277+
per_page = current_app.config.get("SEARCH_ITEMS_PER_PAGE", 25)
278+
username_filter = request.args.get("username")
279+
endpoint_filter = request.args.get("endpoint")
280+
281+
# Validate filters to prevent injection attacks
282+
if username_filter is not None and not isinstance(username_filter, str):
283+
username_filter = None
284+
if endpoint_filter is not None and not isinstance(endpoint_filter, str):
285+
endpoint_filter = None
286+
287+
# Build query based on filters
288+
query = {}
289+
if username_filter:
290+
query["username"] = username_filter
291+
if endpoint_filter:
292+
query["endpoint"] = endpoint_filter
293+
294+
# Get accounting logs sorted by period (most recent first)
295+
logs_cursor = mongo.db.accounting.find(query).sort([("period", pymongo.DESCENDING)])
296+
total = mongo.db.accounting.count_documents(query)
297+
logs_list = list(logs_cursor[(page - 1) * per_page : page * per_page])
298+
299+
# Get distinct usernames and endpoints for filter dropdowns
300+
distinct_usernames = sorted(
301+
[u for u in mongo.db.accounting.distinct("username") if u is not None], key=lambda x: x.lower()
302+
)
303+
distinct_endpoints = sorted(mongo.db.accounting.distinct("endpoint"))
304+
305+
pagination = Pagination(
306+
page=page,
307+
per_page=per_page,
308+
search=False,
309+
found=total,
310+
total=total,
311+
css_framework="bootstrap5",
312+
alignment="center",
313+
)
314+
315+
return render_template(
316+
"admin/accounting.html.jinja2",
317+
logs=logs_list,
318+
pagination=pagination,
319+
distinct_usernames=distinct_usernames,
320+
distinct_endpoints=distinct_endpoints,
321+
username_filter=username_filter,
322+
endpoint_filter=endpoint_filter,
323+
)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{% extends "common/base.html.jinja2" %}
2+
3+
{% block content %}
4+
<main>
5+
<div class="col-10 mx-auto p-3 py-md-5">
6+
<h1>Accounting Logs</h1>
7+
8+
<!-- Filters -->
9+
<form method="GET" class="mb-4">
10+
<div class="row g-3">
11+
<div class="col-md-4">
12+
<label for="username" class="form-label">Username</label>
13+
<select class="form-select" id="username" name="username">
14+
<option value="">All Users</option>
15+
{% for username in distinct_usernames %}
16+
<option value="{{ username }}" {% if username_filter == username %}selected{% endif %}>
17+
{{ username }}
18+
</option>
19+
{% endfor %}
20+
</select>
21+
</div>
22+
<div class="col-md-6">
23+
<label for="endpoint" class="form-label">Endpoint</label>
24+
<select class="form-select" id="endpoint" name="endpoint">
25+
<option value="">All Endpoints</option>
26+
{% for endpoint in distinct_endpoints %}
27+
<option value="{{ endpoint }}" {% if endpoint_filter == endpoint %}selected{% endif %}>
28+
{{ endpoint }}
29+
</option>
30+
{% endfor %}
31+
</select>
32+
</div>
33+
<div class="col-md-2 d-flex align-items-end">
34+
<button type="submit" class="btn btn-primary w-100">Filter</button>
35+
</div>
36+
</div>
37+
</form>
38+
39+
<!-- Results Table -->
40+
<table class="table table-striped">
41+
<thead>
42+
<tr>
43+
<th>Username</th>
44+
<th>IP Address</th>
45+
<th>Endpoint</th>
46+
<th>Period</th>
47+
<th>Count</th>
48+
</tr>
49+
</thead>
50+
<tbody>
51+
{% for log in logs %}
52+
<tr>
53+
<td>{{ log.username or '-' }}</td>
54+
<td>{{ log.ip or '-' }}</td>
55+
<td>{{ log.endpoint }}</td>
56+
<td>{{ log.period.strftime('%Y-%m-%d %H:%M:%S') if log.period else '-' }}</td>
57+
<td>{{ log.count }}</td>
58+
</tr>
59+
{% else %}
60+
<tr>
61+
<td colspan="5">No accounting logs found.</td>
62+
</tr>
63+
{% endfor %}
64+
</tbody>
65+
</table>
66+
67+
{% if pagination %}
68+
<div class="d-flex justify-content-center">
69+
{{ pagination.links }}
70+
</div>
71+
{% endif %}
72+
</div>
73+
</main>
74+
{% endblock %}

sec_certs_page/templates/admin/index.html.jinja2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<a href="{{ url_for("admin.tasks") }}" class="btn btn-primary">Tasks</a>
99
<a href="{{ url_for("admin.config") }}" class="btn btn-primary">Config</a>
1010
<a href="{{ url_for("admin.users") }}" class="btn btn-primary">Users</a>
11+
<a href="{{ url_for("admin.accounting") }}" class="btn btn-primary">Accounting</a>
1112
<a href="/munin/" class="btn btn-primary" target="_blank">Munin</a>
1213
</div>
1314
</div>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[
2+
{
3+
"username": "testuser1",
4+
"ip": null,
5+
"endpoint": "cc.search",
6+
"period": {"$date": "2024-01-01T00:00:00Z"},
7+
"count": 42
8+
},
9+
{
10+
"username": "testuser2",
11+
"ip": null,
12+
"endpoint": "fips.search",
13+
"period": {"$date": "2024-01-01T00:00:00Z"},
14+
"count": 15
15+
},
16+
{
17+
"username": null,
18+
"ip": "192.168.1.1",
19+
"endpoint": "cc.detail",
20+
"period": {"$date": "2024-01-02T00:00:00Z"},
21+
"count": 5
22+
},
23+
{
24+
"username": "testuser1",
25+
"ip": null,
26+
"endpoint": "chat.chat",
27+
"period": {"$date": "2024-01-02T00:00:00Z"},
28+
"count": 100
29+
},
30+
{
31+
"username": null,
32+
"ip": "10.0.0.1",
33+
"endpoint": "fips.detail",
34+
"period": {"$date": "2024-01-03T00:00:00Z"},
35+
"count": 3
36+
}
37+
]

tests/functional/test_admin.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,28 @@ def test_update_diff(logged_in_client):
6363
id = list(mongo.db.fips_diff.find({}, {"_id": True}))[-1]["_id"]
6464
resp = logged_in_client.get(f"/admin/update/diff/{id}")
6565
assert resp.status_code == 200
66+
67+
68+
def test_accounting(logged_in_client):
69+
resp = logged_in_client.get("/admin/accounting")
70+
assert resp.status_code == 200
71+
assert b"Accounting Logs" in resp.data
72+
73+
74+
def test_accounting_filter_by_username(logged_in_client):
75+
resp = logged_in_client.get("/admin/accounting?username=testuser1")
76+
assert resp.status_code == 200
77+
assert b"testuser1" in resp.data
78+
79+
80+
def test_accounting_filter_by_endpoint(logged_in_client):
81+
resp = logged_in_client.get("/admin/accounting?endpoint=cc.search")
82+
assert resp.status_code == 200
83+
assert b"cc.search" in resp.data
84+
85+
86+
def test_accounting_combined_filters(logged_in_client):
87+
resp = logged_in_client.get("/admin/accounting?username=testuser1&endpoint=chat.chat")
88+
assert resp.status_code == 200
89+
assert b"testuser1" in resp.data
90+
assert b"chat.chat" in resp.data

0 commit comments

Comments
 (0)