Skip to content

Commit

Permalink
Finishing up initial release of this code
Browse files Browse the repository at this point in the history
- Adds the remaining prereqs for an ol-django app
- Adds the migration for storing the MaxMind data
- Moves the tests to where they should go
- Added a new management command to create GeoIP2 assignments for private IPv4 nets
  • Loading branch information
jkachel committed Oct 25, 2023
1 parent b10b0bc commit 4b8d740
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 34 deletions.
1 change: 1 addition & 0 deletions src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ target(
"oauth_toolkit_extensions",
"openedx",
"payment_gateway",
"geoip",
]
],
)
26 changes: 26 additions & 0 deletions src/mitol/geoip/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resources(
name="files",
sources=[
"CHANGELOG.md",
"README.md",
"py.typed",
],
)

python_sources(
name="geoip",
sources=["**/*.py"],
dependencies=[
":files",
"//:reqs#setuptools",
],
)

python_distribution(
name="mitol-django-geoip",
dependencies=[":geoip"],
provides=setup_py(
name="mitol-django-geoip",
description="Django application to handle IP-based geolocation",
),
)
5 changes: 5 additions & 0 deletions src/mitol/geoip/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project uses date-based versioning.
38 changes: 21 additions & 17 deletions src/mitol/geoip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,30 @@ client's IP address to it, and it will send you back a good estimation of what
country the client belongs to.

Specifically, this wraps lookup around using a _local_ copy of the MaxMind
GeoIP2 data. This choice was made to avoid having to hit an API during checkout
operations, where doing so may end up blocking the client for an unknown amount
of time.
GeoIP2/GeoLite2 data. This choice was made to avoid having to hit an API during
checkout operations, where doing so may end up blocking the client for an
unknown amount of time.

The `geoip` app provides lookup for a user's country code only so, while you can
use datasets that contain more granular location data in them, you'll still only
get back the country ISO code from the API in this app.

### Setup

You will need a copy of the MaxMind GeoIP2 data in CSV format. This is provided
in several ways:
The MaxMind geolocation databases are provided in two forms:

* For most purposes, the **GeoLite2** dataset is the recommended option. This option provides less features but the dataset is available for free.
* The paid **GeoIP2** dataset can also be used with this library.
* The **GeoLite2** database is free.
* The **GeoIP2** database is not free - a subscription fee applies.

In both cases, you will need the CSV format versions of the databases. The Lite
version is available in ASN, City and Country versions; choose between the City
and Country versions. (The ASN database is not used nor supported.) If the
dataset is only to be used with this app, the Country version is sufficient and
also the smallest.
Which one you need will depend on your use case - however, if your intent is
only to use the dataset with this app, the **GeoLite2** database is sufficient.

In both cases, you will need the CSV format versions of the databases. You have
the option of downloading the ASN, City, or Country versions of the database.
For the purposes of this app, the Country version is sufficient; if you plan on
using the MaxMind data elsewhere in your project and need more granular location
data, you can use the City version instead. The ASN database is not supported
and can be skipped unless you have a specific need for it.

The downloaded datasets include the network block information in two CSV files -
one each for IPv4 and IPv6 network blocks - and the location data in a variety
Expand All @@ -35,9 +38,10 @@ import any others that you require.

After installing the app into your Django project in the normal way, you should
have a management command called `import_maxmind_data` available to you. Run
this command to import the data from the CSV file you downloaded. You _have_ to
this command to import the data from the CSV file you downloaded. You _must_
import the IPv4 netblock file and the English langauge location file but it is
recommended to also import the IPv6 netblock file.
recommended to import the IPv6 netblock file as well, especially if your app
will be capable of serving IPv6 clients.

#### Working Locally

Expand All @@ -46,10 +50,10 @@ datasets don't include the private network blocks in them as they don't really
map to anything, so there's no match for any of the private network IPs.

If you'd like these IPs to match something, you can add that data by running the
`create_private_maxmind_data` command. This will create a fake location record
and netblock records for the 3 private network blocks available:
`create_private_maxmind_data` command. This will create a set of fake netblock
records for the 3 private network blocks available:
* 10.0.0.0/24
* 172.16.0.0/20 _(172.16.0.0-172.31.255.255)_
* 172.16.0.0/12 _(172.16.0.0-172.31.255.255)_
* 192.168.0.0/16

You can specify what ISO code you wish to assign to these as well. Running the
Expand Down
2 changes: 1 addition & 1 deletion src/mitol/geoip/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.contrib import admin

from geoip import models
from mitol.geoip import models


class NetBlockAdmin(admin.ModelAdmin):
Expand Down
3 changes: 1 addition & 2 deletions src/mitol/geoip/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from django.db import transaction
from django.db.models import Q

from geoip import models

from mitol.geoip import models

MAXMIND_CSV_COUNTRY_LOCATIONS_LITE = "geolite2-country-locations"
MAXMIND_CSV_COUNTRY_BLOCKS_IPV4_LITE = "geolite2-country-ipv4"
Expand Down
19 changes: 15 additions & 4 deletions src/mitol/geoip/apps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
from django.apps import AppConfig
"""GeoIP app AppConfigs"""
import os

from mitol.common.apps import BaseApp

class GeoipConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "geoip"

class GeoIPApp(BaseApp):
"""Default configuration for the GeoIP app"""

name = "mitol.geoip"
label = "geoip"
verbose_name = "GeoIP"

required_settings = []

# necessary because this is a namespaced app
path = os.path.dirname(os.path.abspath(__file__))
37 changes: 37 additions & 0 deletions src/mitol/geoip/changelog.d/20231025_162713_jkachel_geoip_app.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.. A new scriv changelog fragment.
..
.. Uncomment the header that is right (remove the leading dots).
..
.. Removed
.. -------
..
.. - A bullet item for the Removed category.
..
Added
-----

- Migrated the MaxMind GeoIP code from xPRO into a ol-django app.
- Added a new management command (`create_private_maxmind_data`) to create GeoIP data for local networks.

..
.. Changed
.. -------
..
.. - A bullet item for the Changed category.
..
.. Deprecated
.. ----------
..
.. - A bullet item for the Deprecated category.
..
.. Fixed
.. -----
..
.. - A bullet item for the Fixed category.
..
.. Security
.. --------
..
.. - A bullet item for the Security category.
..
3 changes: 1 addition & 2 deletions src/mitol/geoip/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from factory import LazyAttribute, LazyFunction, fuzzy
from factory.django import DjangoModelFactory

from geoip import models

from mitol.geoip import models

fake = faker.Faker()

Expand Down
75 changes: 75 additions & 0 deletions src/mitol/geoip/management/commands/create_private_maxmind_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
Creates a set of netblocks for the private IPv4 netblocks, which aren't in the
MaxMind dataset.
The ISO code must match one that we've imported from MaxMind, so if you haven't
imported the geonames data yet, this will not work (since we have to map the
created netblock to a geoname anyway).
"""

import ipaddress
from decimal import Decimal

from django.core.management import BaseCommand, CommandError

from mitol.geoip.models import Geoname, NetBlock


class Command(BaseCommand):
"""
Creates MaxMind assignments for the private IPv4 netblocks.
"""

help = "Creates MaxMind assignments for the private IPv4 netblocks."
MAX_BIGINTEGERFIELD = 9223372036854775807

def add_arguments(self, parser) -> None:
parser.add_argument(
"iso",
type=str,
help="The ISO 3166 alpha2 code to assign these to.",
)

parser.add_argument(
"--remove",
action="store_true",
help="Remove the local address netblocks rather than create them.",
)

def handle(self, *args, **kwargs):
try:
geoname = Geoname.objects.filter(country_iso_code=kwargs["iso"]).first()
except Geoname.DoesNotExist:
raise CommandError(
f"Could not find a Geoname record for {kwargs['iso']} - have you imported the MaxMind databases?"
)

netblocks = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]

if "remove" in kwargs and kwargs["remove"]:
removed_count = NetBlock.objects.filter(network__in=netblocks).delete()

self.stdout.write(
self.style.SUCCESS(f"{removed_count[0]} netblocks removed!")
)
else:
for netblock in netblocks:
network = ipaddress.ip_network(netblock)

(_, created) = NetBlock.objects.update_or_create(
network=netblock,
defaults={
"is_ipv6": False,
"decimal_ip_start": Decimal(int(network[0])),
"decimal_ip_end": Decimal(int(network[-1])),
"ip_start": network[0],
"ip_end": network[-1],
"geoname_id": geoname.geoname_id,
},
)

self.stdout.write(
self.style.SUCCESS(
f"{'Created' if created else 'Updated'} record for {netblock} for ISO {kwargs['iso']}"
)
)
5 changes: 3 additions & 2 deletions src/mitol/geoip/management/commands/import_maxmind_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
API call that does.)
"""

from django.core.management import BaseCommand, CommandError
from os import path

from maxmind import api
from django.core.management import BaseCommand, CommandError

from mitol.geoip import api


class Command(BaseCommand):
Expand Down
Loading

0 comments on commit 4b8d740

Please sign in to comment.