Skip to content

Commit ca0ff00

Browse files
authored
Admin: Link unique logins with IPs in detail views (#19058)
* Route admin IP address view by IP address * Link admin unique login IP addresses * List unique logins on IP address page * Update tests
1 parent b63fe4b commit ca0ff00

File tree

6 files changed

+72
-17
lines changed

6 files changed

+72
-17
lines changed

tests/unit/admin/test_routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def test_includeme():
225225
pretend.call("admin.ip_address.list", "/admin/ip-addresses/", domain=warehouse),
226226
pretend.call(
227227
"admin.ip_address.detail",
228-
"/admin/ip-addresses/{ip_address_id}",
228+
"/admin/ip-addresses/{ip_address}",
229229
domain=warehouse,
230230
),
231231
pretend.call("admin.project.list", "/admin/projects/", domain=warehouse),

tests/unit/admin/views/test_ipaddresses.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3-
import uuid
4-
53
import pretend
64
import pytest
75

86
from pyramid.httpexceptions import HTTPBadRequest
97

8+
from tests.common.db.accounts import UserUniqueLoginFactory
109
from tests.common.db.ip_addresses import IpAddressFactory
1110
from warehouse.admin.views import ip_addresses as ip_views
1211

@@ -45,21 +44,32 @@ def test_with_invalid_page(self):
4544

4645
class TestIpAddressDetail:
4746
def test_no_ip_address(self, db_request):
48-
db_request.matchdict["ip_address_id"] = None
47+
db_request.matchdict["ip_address"] = None
4948

5049
with pytest.raises(HTTPBadRequest):
5150
ip_views.ip_address_detail(db_request)
5251

5352
def test_ip_address_not_found(self, db_request):
54-
db_request.matchdict["ip_address_id"] = uuid.uuid4()
53+
db_request.matchdict["ip_address"] = "69.69.69.69"
5554

5655
with pytest.raises(HTTPBadRequest):
5756
ip_views.ip_address_detail(db_request)
5857

59-
def test_ip_address_found(self, db_request):
58+
def test_ip_address_found_no_unique_logins(self, db_request):
6059
ip_address = IpAddressFactory()
61-
db_request.matchdict["ip_address_id"] = ip_address.id
60+
db_request.matchdict["ip_address"] = str(ip_address.ip_address)
61+
62+
result = ip_views.ip_address_detail(db_request)
63+
64+
assert result == {"ip_address": ip_address, "unique_logins": []}
65+
66+
def test_ip_address_found_with_unique_logins(self, db_request):
67+
ip_address = IpAddressFactory()
68+
unique_login = UserUniqueLoginFactory.create(
69+
ip_address=str(ip_address.ip_address)
70+
)
71+
db_request.matchdict["ip_address"] = str(ip_address.ip_address)
6272

6373
result = ip_views.ip_address_detail(db_request)
6474

65-
assert result == {"ip_address": ip_address}
75+
assert result == {"ip_address": ip_address, "unique_logins": [unique_login]}

warehouse/admin/routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def includeme(config):
227227
config.add_route("admin.ip_address.list", "/admin/ip-addresses/", domain=warehouse)
228228
config.add_route(
229229
"admin.ip_address.detail",
230-
"/admin/ip-addresses/{ip_address_id}",
230+
"/admin/ip-addresses/{ip_address}",
231231
domain=warehouse,
232232
)
233233

warehouse/admin/templates/admin/ip_addresses/detail.html

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
{% import "admin/utils/pagination.html" as pagination %}
66

7-
{% block title %}{{ ip_address.id }}{% endblock %}
7+
{% block title %}{{ ip_address }}{% endblock %}
88

99
{% block breadcrumb %}
1010
<li class="breadcrumb-item"><a href="{{ request.route_path('admin.ip_address.list') }}">IP Addresses</a></li>
11-
<li class="breadcrumb-item active">{{ ip_address.id }}</li>
11+
<li class="breadcrumb-item active">{{ ip_address }}</li>
1212
{% endblock %}
1313

1414
{% block content %}
@@ -22,6 +22,9 @@ <h3 class="card-title"><code>IpAddress</code> Record</h3>
2222
<dt class="col-sm-4">IP Address:</dt>
2323
<dd class="col-sm-8">{{ ip_address.ip_address }}</dd>
2424

25+
<dt class="col-sm-4">IP Address ID:</dt>
26+
<dd class="col-sm-8">{{ ip_address.ip_address.id }}</dd>
27+
2528
<dt class="col-sm-4">Hashed IP Address:</dt>
2629
<dd class="col-sm-8">{{ ip_address.hashed_ip_address }}</dd>
2730

@@ -38,5 +41,40 @@ <h3 class="card-title"><code>IpAddress</code> Record</h3>
3841
<dd class="col-sm-8">{{ ip_address.ban_reason.value }}</dd>
3942
</dl>
4043
</div> <!-- /.card-body -->
41-
</div>
44+
</div> <!-- /.card -->
45+
46+
<div class="card">
47+
<div class="card-header with-border">
48+
<h3 class="card-title">Unique logins</h3>
49+
</div>
50+
51+
<div class="card-body">
52+
{% if unique_logins %}
53+
<div class="table-responsive p-0">
54+
<table class="table table-hover" id="pending-oidc-publishers">
55+
<thead>
56+
<tr>
57+
<th scope="col">Created</th>
58+
<th scope="col">User</th>
59+
<th scope="col">Status</th>
60+
<th scope="col">Device Information</th>
61+
</tr>
62+
</thead>
63+
<tbody>
64+
{% for login in unique_logins %}
65+
<tr>
66+
<td>{{ login.created }}</td>
67+
<td><a href="{{ request.route_path('admin.user.detail', username=login.user.username ) }}">{{ login.user.username }}</a></td>
68+
<td>{{ login.status.value }}</td>
69+
<td>{{ login.device_information }}</td>
70+
</tr>
71+
{% endfor %}
72+
</tbody>
73+
</table>
74+
</div>
75+
{% else %}
76+
No known logins.
77+
{% endif %}
78+
</div> <!-- /.card-body -->
79+
</div> <!-- /.card -->
4280
{% endblock %}

warehouse/admin/templates/admin/users/detail.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,7 @@ <h3 class="card-title">Unique logins</h3>
10091009
{% for login in user.unique_logins %}
10101010
<tr>
10111011
<td>{{ login.created }}</td>
1012-
<td>{{ login.ip_address }}</td>
1012+
<td><a href="{{ request.route_path('admin.ip_address.detail', ip_address=login.ip_address ) }}">{{ login.ip_address }}</a></td>
10131013
<td>{{ login.status.value }}</td>
10141014
<td>{{ login.device_information }}</td>
10151015
</tr>

warehouse/admin/views/ip_addresses.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pyramid.view import view_config
1010
from sqlalchemy.exc import NoResultFound
1111

12+
from warehouse.accounts.models import UserUniqueLogin
1213
from warehouse.authnz import Permissions
1314
from warehouse.ip_addresses.models import IpAddress
1415
from warehouse.utils.paginate import paginate_url_factory
@@ -51,10 +52,16 @@ def ip_address_list(request: Request) -> dict[str, SQLAlchemyORMPage[IpAddress]
5152
uses_session=True,
5253
)
5354
def ip_address_detail(request: Request) -> dict[str, IpAddress]:
54-
ip_address_id = request.matchdict["ip_address_id"]
55+
ip_address = request.matchdict["ip_address"]
5556
try:
56-
ip_address = request.db.query(IpAddress).filter_by(id=ip_address_id).one()
57+
ip_address = request.db.query(IpAddress).filter_by(ip_address=ip_address).one()
5758
except NoResultFound:
58-
raise HTTPBadRequest("No IP Address found with that id.")
59+
raise HTTPBadRequest("No matching IP Address found.")
5960

60-
return {"ip_address": ip_address}
61+
unique_logins = (
62+
request.db.query(UserUniqueLogin)
63+
.filter(UserUniqueLogin.ip_address == str(ip_address.ip_address))
64+
.all()
65+
)
66+
67+
return {"ip_address": ip_address, "unique_logins": unique_logins}

0 commit comments

Comments
 (0)