Skip to content

Commit 5175a95

Browse files
authored
feat: use geoip info for display (#13745)
* feat: use geoip info for display Basic implementation of pulling the GeoIP info off `Event.additional` Needs test cases, as well as refining the display order of values. Signed-off-by: Mike Fiedler <[email protected]> * make translations Signed-off-by: Mike Fiedler <[email protected]> --------- Signed-off-by: Mike Fiedler <[email protected]>
1 parent 0ae1752 commit 5175a95

File tree

10 files changed

+171
-20
lines changed

10 files changed

+171
-20
lines changed

tests/unit/events/test_models.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import pytest
14+
15+
from warehouse.events.models import GeoIPInfo
16+
17+
18+
class TestGeoIPInfo:
19+
@pytest.mark.parametrize(
20+
"test_input, expected",
21+
[
22+
({}, ""),
23+
(
24+
{"city": "new york", "country_code": "us", "region": "ny"},
25+
"New York, NY, US",
26+
),
27+
(
28+
{"city": "new york", "country_code": "us"},
29+
"New York, US",
30+
),
31+
({"city": "new york", "region": "ny"}, "New York, NY"),
32+
({"region": "ny", "country_code": "us"}, "NY, US"),
33+
({"country_name": "United States"}, "United States"),
34+
],
35+
)
36+
def test_display_output(self, test_input, expected):
37+
"""Create a GeoIPInfo object and test the display method."""
38+
dataklazz = GeoIPInfo(**test_input)
39+
assert dataklazz.display() == expected

tests/unit/organizations/test_models.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,14 +183,14 @@ def test_acl(self, db_session):
183183
key=lambda x: x[1],
184184
)
185185

186-
def test_record_event(self, db_request):
186+
def test_record_event_with_geoip(self, db_request):
187187
"""
188188
Test to cover condition when record_event is called with geoip_info as
189189
part of the inbound request.
190190
Possibly could be removed once more comprehensive tests are in place,
191191
but nothing explicitly covers `HasEvents.record_event`
192192
"""
193-
db_request.ip_address.geoip_info = {"country_code": "US"}
193+
db_request.ip_address.geoip_info = {"country_name": "United States"}
194194

195195
organization = DBOrganizationFactory.create()
196196

@@ -201,6 +201,49 @@ def test_record_event(self, db_request):
201201
additional={},
202202
)
203203

204+
event = organization.events[0]
205+
206+
assert event.additional == {
207+
"organization_name": organization.name,
208+
"geoip_info": {"country_name": "United States"},
209+
}
210+
assert event.location_info == "United States"
211+
212+
def test_location_info_without_geoip(self, db_request):
213+
organization = DBOrganizationFactory.create()
214+
organization.record_event(
215+
tag="",
216+
ip_address=db_request.ip_address.ip_address,
217+
request=db_request,
218+
additional={},
219+
)
220+
221+
event = organization.events[0]
222+
223+
assert event.additional == {
224+
"organization_name": organization.name,
225+
}
226+
assert event.location_info == db_request.ip_address
227+
228+
def test_location_info_with_partial(self, db_request):
229+
db_request.ip_address.geoip_info = {"country_code3": "USA"}
230+
231+
organization = DBOrganizationFactory.create()
232+
organization.record_event(
233+
tag="",
234+
ip_address=db_request.ip_address.ip_address,
235+
request=db_request,
236+
additional={},
237+
)
238+
239+
event = organization.events[0]
240+
241+
assert event.additional == {
242+
"organization_name": organization.name,
243+
"geoip_info": {"country_code3": "USA"},
244+
}
245+
assert event.location_info == db_request.ip_address
246+
204247

205248
class TestTeamFactory:
206249
def test_traversal_finds(self, db_request):

warehouse/admin/templates/admin/projects/detail.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ <h3 class="card-title">Project activity</h3>
321321
<th scope="col">Time</th>
322322
<th scope="col">IP address</th>
323323
<th scope="col">Hashed IP address</th>
324+
<th scope="col">Location Info</th>
324325
<th scope="col">Additional information</th>
325326
</tr>
326327
</thead>
@@ -331,6 +332,7 @@ <h3 class="card-title">Project activity</h3>
331332
<td>{{ event.time }}</td>
332333
<td>{{ event.ip_address }}</td>
333334
<td>{{ event.ip_address.hashed_ip_address }}</td>
335+
<td>{{ event.location_info }}</td>
334336
<td>{{ event.additional }}</td>
335337
</tr>
336338
{% endfor %}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ <h3 class="card-title">Account activity</h3>
458458
<th scope="col">Time</th>
459459
<th scope="col">IP address</th>
460460
<th scope="col">Hashed IP address</th>
461+
<th scope="col">Location info</th>
461462
<th scope="col">Additional information</th>
462463
</tr>
463464
</thead>
@@ -468,6 +469,7 @@ <h3 class="card-title">Account activity</h3>
468469
<td>{{ event.time }}</td>
469470
<td>{{ event.ip_address }}</td>
470471
<td>{{ event.ip_address.hashed_ip_address }}</td>
472+
<td>{{ event.location_info }}</td>
471473
<td>{{ event.additional }}</td>
472474
</tr>
473475
{% endfor %}

warehouse/events/models.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
import typing
1515

16+
from dataclasses import dataclass
17+
1618
from sqlalchemy import Column, DateTime, ForeignKey, Index, String, orm, sql
1719
from sqlalchemy.dialects.postgresql import JSONB, UUID
1820
from sqlalchemy.exc import NoResultFound
@@ -27,6 +29,49 @@
2729
from pyramid.request import Request
2830

2931

32+
@dataclass
33+
class GeoIPInfo:
34+
"""
35+
Optional values passed in from upstream CDN
36+
https://github.com/pypi/infra/blob/3e57592ba91205cb5d10c588d529767b101753cd/terraform/warehouse/vcl/main.vcl#L181-L191
37+
"""
38+
39+
city: str | None = None
40+
continent_code: str | None = None
41+
country_code3: str | None = None
42+
country_code: str | None = None
43+
country_name: str | None = None
44+
region: str | None = None
45+
46+
@property
47+
def _city(self) -> str:
48+
return self.city.title() if self.city else ""
49+
50+
@property
51+
def _region(self) -> str:
52+
return self.region.upper() if self.region else ""
53+
54+
@property
55+
def _country_code(self) -> str:
56+
return self.country_code.upper() if self.country_code else ""
57+
58+
def display(self) -> str:
59+
"""
60+
Construct a reasonable location, depending on optional values
61+
"""
62+
if self.city and self.region and self.country_code:
63+
return f"{self._city}, {self._region}, {self._country_code}"
64+
elif self.city and self.region:
65+
return f"{self._city}, {self._region}"
66+
elif self.city and self.country_code:
67+
return f"{self._city}, {self._country_code}"
68+
elif self.region:
69+
return f"{self._region}, {self._country_code}"
70+
elif self.country_name:
71+
return self.country_name
72+
return ""
73+
74+
3075
class Event(AbstractConcreteBase):
3176
tag = Column(String, nullable=False)
3277
time = Column(DateTime, nullable=False, server_default=sql.func.now())
@@ -99,6 +144,23 @@ def ip_address(cls, value): # noqa: N805
99144
session.add(_ip_address)
100145
cls.ip_address_obj = _ip_address
101146

147+
@property
148+
def location_info(cls) -> str: # noqa: N805
149+
"""
150+
Determine "best" location info to display.
151+
152+
Dig into `.additional` for `geoip_info` and return that if it exists.
153+
It was stored at the time opf the event, and may change in the related
154+
`IpAddress` object over time.
155+
Otherwise, return the `ip_address_obj` and let its repr decide.
156+
"""
157+
if cls.additional is not None and "geoip_info" in cls.additional:
158+
g = GeoIPInfo(**cls.additional["geoip_info"])
159+
if g.display():
160+
return g.display()
161+
162+
return cls.ip_address_obj
163+
102164
def __init_subclass__(cls, /, parent_class, **kwargs):
103165
cls._parent_class = parent_class
104166
return cls

warehouse/locale/messages.pot

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3356,13 +3356,8 @@ msgstr ""
33563356

33573357
#: warehouse/templates/manage/account.html:685
33583358
#: warehouse/templates/manage/account.html:696
3359-
#: warehouse/templates/manage/organization/history.html:203
33603359
#: warehouse/templates/manage/organization/history.html:215
3361-
#: warehouse/templates/manage/project/history.html:290
3362-
#: warehouse/templates/manage/project/history.html:302
3363-
#: warehouse/templates/manage/team/history.html:110
3364-
#: warehouse/templates/manage/team/history.html:122
3365-
msgid "IP address"
3360+
msgid "Location Info"
33663361
msgstr ""
33673362

33683363
#: warehouse/templates/manage/account.html:704
@@ -4666,6 +4661,14 @@ msgstr ""
46664661
msgid "Time"
46674662
msgstr ""
46684663

4664+
#: warehouse/templates/manage/organization/history.html:203
4665+
#: warehouse/templates/manage/project/history.html:290
4666+
#: warehouse/templates/manage/project/history.html:302
4667+
#: warehouse/templates/manage/team/history.html:110
4668+
#: warehouse/templates/manage/team/history.html:122
4669+
msgid "Location info"
4670+
msgstr ""
4671+
46694672
#: warehouse/templates/manage/organization/manage_organization_base.html:20
46704673
#, python-format
46714674
msgid "Manage '%(organization_name)s'"

warehouse/templates/manage/account.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
682682
<thead>
683683
<th scope="col">{% trans %}Event{% endtrans %}</th>
684684
<th scope="col">{% trans %}Date / time{% endtrans %}</th>
685-
<th scope="col">{% trans %}IP address{% endtrans %}</th>
685+
<th scope="col">{% trans %}Location Info{% endtrans %}</th>
686686
</thead>
687687
<tbody>
688688
{% for event in recent_events %}
@@ -693,8 +693,8 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
693693
{{ humanize(event.time, time="true") }}
694694
</td>
695695
<td>
696-
<span class="table__mobile-label">{% trans %}IP address{% endtrans %}</span>
697-
{{ "Redacted" if event.additional.redact_ip else event.ip_address }}
696+
<span class="table__mobile-label">{% trans %}Location Info{% endtrans %}</span>
697+
{{ "Redacted" if event.additional.redact_ip else event.location_info }}
698698
</td>
699699
</tr>
700700
{% endfor %}

warehouse/templates/manage/organization/history.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
200200
<tr>
201201
<th scope="col">{% trans %}Event{% endtrans %}</th>
202202
<th scope="col">{% trans %}Time{% endtrans %}</th>
203-
<th scope="col">{% trans %}IP address{% endtrans %}</th>
203+
<th scope="col">{% trans %}Location info{% endtrans %}</th>
204204
</tr>
205205
</thead>
206206
<tbody>
@@ -212,8 +212,8 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
212212
{{ humanize(event.time, time="true") }}
213213
</td>
214214
<td>
215-
<span class="table__mobile-label">{% trans %}IP address{% endtrans %}</span>
216-
{{ event.ip_address }}
215+
<span class="table__mobile-label">{% trans %}Location Info{% endtrans %}</span>
216+
{{ event.location_info }}
217217
</td>
218218
</tr>
219219
{% endfor %}

warehouse/templates/manage/project/history.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
287287
<tr>
288288
<th scope="col">{% trans %}Event{% endtrans %}</th>
289289
<th scope="col">{% trans %}Time{% endtrans %}</th>
290-
<th scope="col">{% trans %}IP address{% endtrans %}</th>
290+
<th scope="col">{% trans %}Location info{% endtrans %}</th>
291291
</tr>
292292
</thead>
293293
<tbody>
@@ -299,8 +299,8 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
299299
{{ humanize(event.time, time="true") }}
300300
</td>
301301
<td>
302-
<span class="table__mobile-label">{% trans %}IP address{% endtrans %}</span>
303-
{{ event.ip_address }}
302+
<span class="table__mobile-label">{% trans %}Location info{% endtrans %}</span>
303+
{{ event.location_info }}
304304
</td>
305305
</tr>
306306
{% endfor %}

warehouse/templates/manage/team/history.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
107107
<tr>
108108
<th scope="col">{% trans %}Event{% endtrans %}</th>
109109
<th scope="col">{% trans %}Time{% endtrans %}</th>
110-
<th scope="col">{% trans %}IP address{% endtrans %}</th>
110+
<th scope="col">{% trans %}Location info{% endtrans %}</th>
111111
</tr>
112112
</thead>
113113
<tbody>
@@ -119,8 +119,8 @@ <h2>{% trans %}Security history{% endtrans %}</h2>
119119
{{ humanize(event.time, time="true") }}
120120
</td>
121121
<td>
122-
<span class="table__mobile-label">{% trans %}IP address{% endtrans %}</span>
123-
{{ event.ip_address }}
122+
<span class="table__mobile-label">{% trans %}Location info{% endtrans %}</span>
123+
{{ event.location_info }}
124124
</td>
125125
</tr>
126126
{% endfor %}

0 commit comments

Comments
 (0)