policies: add GeoIP policy (#10454)

* add GeoIP policy

* handle empty lists of ASNs and countries

* handle missing GeoIP database or missing IP from the database

The exceptions raised here are `PolicyException`s to let admins bypass
an execution failure.

* fix translations

whoops

* remove `GeoIPPolicyMode`

Use the policy binding's `negate` option instead

* fix `DataProvision` typing

`ak-dual-select-provider` can handle unpaginated data

* use `django-countries` instead of a static list of countries for ISO-3166

* simplify `GeoIPPolicyForm`

* pass `GeoIPPolicy` on empty policy

* add backend tests to `GeoIPPolicy`

* revise translations

* move `iso-3166/` to `policies/geoip_iso3166/`

* add client-side caching to ISO3166 API call

* fix `GeoIPPolicy` creation

The automatically generated APIs can't seem to handle `CountryField`,
so I'll have to do this by hand too.

* add docs for GeoIP Policy

* docs: stylize

add review suggestions from @tanberry

* refactor `GeoIPPolicy` API

It is now as declarative as I could make it.

* clean up `api.py` and `views.py`
This commit is contained in:
Simonyi Gergő
2024-08-06 12:37:29 +02:00
committed by GitHub
parent 87858afaf3
commit f7b16ed723
22 changed files with 1650 additions and 10 deletions

View File

View File

@ -0,0 +1,55 @@
"""GeoIP Policy API Views"""
from django_countries import countries
from django_countries.serializer_fields import CountryField
from django_countries.serializers import CountryFieldMixin
from rest_framework import serializers
from rest_framework.generics import ListAPIView
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.geoip.models import GeoIPPolicy
from authentik.policies.geoip.serializer_fields import DetailedCountryField
class DetailedCountrySerializer(serializers.Serializer):
code = CountryField()
name = serializers.CharField()
class ISO3166View(ListAPIView):
"""Get all countries in ISO-3166-1"""
permission_classes = [AllowAny]
queryset = [{"code": code, "name": name} for (code, name) in countries]
serializer_class = DetailedCountrySerializer
filter_backends = []
pagination_class = None
class GeoIPPolicySerializer(CountryFieldMixin, PolicySerializer):
"""GeoIP Policy Serializer"""
countries_obj = serializers.ListField(
child=DetailedCountryField(), source="countries", read_only=True
)
class Meta:
model = GeoIPPolicy
fields = PolicySerializer.Meta.fields + [
"asns",
"countries",
"countries_obj",
]
class GeoIPPolicyViewSet(UsedByMixin, ModelViewSet):
"""GeoIP Viewset"""
queryset = GeoIPPolicy.objects.all()
serializer_class = GeoIPPolicySerializer
filterset_fields = ["name"]
ordering = ["name"]
search_fields = ["name"]

View File

@ -0,0 +1,11 @@
"""Authentik policy geoip app config"""
from django.apps import AppConfig
class AuthentikPolicyGeoIPConfig(AppConfig):
"""Authentik policy_geoip app config"""
name = "authentik.policies.geoip"
label = "authentik_policies_geoip"
verbose_name = "authentik Policies.GeoIP"

View File

@ -0,0 +1,5 @@
from authentik.lib.sentry import SentryIgnoredException
class GeoIPNotFoundException(SentryIgnoredException):
"""Exception raised when an IP is not found in a GeoIP database"""

View File

@ -0,0 +1,52 @@
# Generated by Django 5.0.7 on 2024-07-16 11:23
import django.contrib.postgres.fields
import django.db.models.deletion
import django_countries.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_policies", "0011_policybinding_failure_result_and_more"),
]
operations = [
migrations.CreateModel(
name="GeoIPPolicy",
fields=[
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policy",
),
),
(
"asns",
django.contrib.postgres.fields.ArrayField(
base_field=models.IntegerField(), blank=True, default=list, size=None
),
),
(
"countries",
django_countries.fields.CountryField(blank=True, max_length=746, multiple=True),
),
],
options={
"verbose_name": "GeoIP Policy",
"verbose_name_plural": "GeoIP Policies",
"indexes": [
models.Index(fields=["policy_ptr_id"], name="authentik_p_policy__5cc4a9_idx")
],
},
bases=("authentik_policies.policy",),
),
]

View File

@ -0,0 +1,92 @@
"""GeoIP policy"""
from itertools import chain
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext as _
from django_countries.fields import CountryField
from rest_framework.serializers import BaseSerializer
from authentik.policies.exceptions import PolicyException
from authentik.policies.geoip.exceptions import GeoIPNotFoundException
from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult
class GeoIPPolicy(Policy):
"""Ensure the user satisfies requirements of geography or network topology, based on IP
address."""
asns = ArrayField(models.IntegerField(), blank=True, default=list)
countries = CountryField(multiple=True, blank=True)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.policies.geoip.api import GeoIPPolicySerializer
return GeoIPPolicySerializer
@property
def component(self) -> str: # pragma: no cover
return "ak-policy-geoip-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""
Passes if any of the following is true:
- the client IP is advertised by an autonomous system with ASN in the `asns`
- the client IP is geolocated in a country of `countries`
"""
results: list[PolicyResult] = []
if self.asns:
results.append(self.passes_asn(request))
if self.countries:
results.append(self.passes_country(request))
if not results:
return PolicyResult(True)
passing = any(r.passing for r in results)
messages = chain(*[r.messages for r in results])
result = PolicyResult(passing, *messages)
result.source_results = results
return result
def passes_asn(self, request: PolicyRequest) -> PolicyResult:
# This is not a single get chain because `request.context` can contain `{ "asn": None }`.
asn_data = request.context.get("asn")
asn = asn_data.get("asn") if asn_data else None
if not asn:
raise PolicyException(
GeoIPNotFoundException(_("GeoIP: client IP not found in ASN database."))
)
if asn not in self.asns:
message = _("Client IP is not part of an allowed autonomous system.")
return PolicyResult(False, message)
return PolicyResult(True)
def passes_country(self, request: PolicyRequest) -> PolicyResult:
# This is not a single get chain because `request.context` can contain `{ "geoip": None }`.
geoip_data = request.context.get("geoip")
country = geoip_data.get("country") if geoip_data else None
if not country:
raise PolicyException(
GeoIPNotFoundException(_("GeoIP: client IP address not found in City database."))
)
if country not in self.countries:
message = _("Client IP is not in an allowed country.")
return PolicyResult(False, message)
return PolicyResult(True)
class Meta(Policy.PolicyMeta):
verbose_name = _("GeoIP Policy")
verbose_name_plural = _("GeoIP Policies")

View File

@ -0,0 +1,21 @@
"""Workaround for https://github.com/SmileyChris/django-countries/issues/441"""
from django_countries.serializer_fields import CountryField
from drf_spectacular.utils import extend_schema_field, inline_serializer
from rest_framework import serializers
DETAILED_COUNTRY_SCHEMA = {
"code": CountryField(),
"name": serializers.CharField(),
}
@extend_schema_field(
inline_serializer(
"DetailedCountryField",
DETAILED_COUNTRY_SCHEMA,
)
)
class DetailedCountryField(CountryField):
def __init__(self):
super().__init__(country_dict=True)

View File

@ -0,0 +1,128 @@
"""geoip policy tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.policies.engine import PolicyRequest, PolicyResult
from authentik.policies.exceptions import PolicyException
from authentik.policies.geoip.exceptions import GeoIPNotFoundException
from authentik.policies.geoip.models import GeoIPPolicy
class TestGeoIPPolicy(TestCase):
"""Test GeoIP Policy"""
def setUp(self):
super().setUp()
self.request = PolicyRequest(get_anonymous_user())
self.context_disabled_geoip = {}
self.context_unknown_ip = {"asn": None, "geoip": None}
# 8.8.8.8
self.context = {
"asn": {"asn": 15169, "as_org": "GOOGLE", "network": "8.8.8.0/24"},
"geoip": {
"continent": "NA",
"country": "US",
"lat": 37.751,
"long": -97.822,
"city": "",
},
}
self.matching_asns = [13335, 15169]
self.matching_countries = ["US", "CA"]
self.mismatching_asns = [1, 2]
self.mismatching_countries = ["MX", "UA"]
def enrich_context_disabled_geoip(self):
pass
def enrich_context_unknown_ip(self):
self.request.context["asn"] = self.context_unknown_ip["asn"]
self.request.context["geoip"] = self.context_unknown_ip["geoip"]
def enrich_context(self):
self.request.context["asn"] = self.context["asn"]
self.request.context["geoip"] = self.context["geoip"]
def test_disabled_geoip(self):
"""Test that disabled GeoIP raises PolicyException with GeoIPNotFoundException"""
self.enrich_context_disabled_geoip()
policy = GeoIPPolicy.objects.create(
asns=self.matching_asns, countries=self.matching_countries
)
with self.assertRaises(PolicyException) as cm:
policy.passes(self.request)
self.assertIsInstance(cm.exception.src_exc, GeoIPNotFoundException)
def test_unknown_ip(self):
"""Test that unknown IP raises PolicyException with GeoIPNotFoundException"""
self.enrich_context_unknown_ip()
policy = GeoIPPolicy.objects.create(
asns=self.matching_asns, countries=self.matching_countries
)
with self.assertRaises(PolicyException) as cm:
policy.passes(self.request)
self.assertIsInstance(cm.exception.src_exc, GeoIPNotFoundException)
def test_empty_policy(self):
"""Test that empty policy passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create()
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_policy_with_matching_asns(self):
"""Test that a policy with matching ASNs passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(asns=self.matching_asns)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_policy_with_mismatching_asns(self):
"""Test that a policy with mismatching ASNs fails"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(asns=self.mismatching_asns)
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)
def test_policy_with_matching_countries(self):
"""Test that a policy with matching countries passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(countries=self.matching_countries)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_policy_with_mismatching_countries(self):
"""Test that a policy with mismatching countries fails"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(countries=self.mismatching_countries)
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)
def test_policy_requires_only_one_match(self):
"""Test that a policy with one matching value passes"""
self.enrich_context()
policy = GeoIPPolicy.objects.create(
asns=self.mismatching_asns, countries=self.matching_countries
)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)

View File

@ -0,0 +1,10 @@
"""API URLs"""
from django.urls import path
from authentik.policies.geoip.api import GeoIPPolicyViewSet, ISO3166View
api_urlpatterns = [
("policies/geoip", GeoIPPolicyViewSet),
path("policies/geoip_iso3166/", ISO3166View.as_view(), name="iso-3166-view"),
]

View File

@ -59,6 +59,7 @@ SHARED_APPS = [
"django_filters", "django_filters",
"drf_spectacular", "drf_spectacular",
"django_prometheus", "django_prometheus",
"django_countries",
"pgactivity", "pgactivity",
"pglock", "pglock",
"channels", "channels",
@ -76,6 +77,7 @@ TENANT_APPS = [
"authentik.policies.event_matcher", "authentik.policies.event_matcher",
"authentik.policies.expiry", "authentik.policies.expiry",
"authentik.policies.expression", "authentik.policies.expression",
"authentik.policies.geoip",
"authentik.policies.password", "authentik.policies.password",
"authentik.policies.reputation", "authentik.policies.reputation",
"authentik.policies", "authentik.policies",
@ -146,6 +148,7 @@ SPECTACULAR_SETTINGS = {
"url": "https://github.com/goauthentik/authentik/blob/main/LICENSE", "url": "https://github.com/goauthentik/authentik/blob/main/LICENSE",
}, },
"ENUM_NAME_OVERRIDES": { "ENUM_NAME_OVERRIDES": {
"CountryCodeEnum": "django_countries.countries",
"EventActions": "authentik.events.models.EventAction", "EventActions": "authentik.events.models.EventAction",
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation", "FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
"FlowLayoutEnum": "authentik.flows.models.FlowLayout", "FlowLayoutEnum": "authentik.flows.models.FlowLayout",

View File

@ -481,6 +481,46 @@
} }
} }
}, },
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_policies_geoip.geoippolicy"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_policies_geoip.geoippolicy_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_policies_geoip.geoippolicy"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_policies_geoip.geoippolicy"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@ -3979,6 +4019,7 @@
"authentik.policies.event_matcher", "authentik.policies.event_matcher",
"authentik.policies.expiry", "authentik.policies.expiry",
"authentik.policies.expression", "authentik.policies.expression",
"authentik.policies.geoip",
"authentik.policies.password", "authentik.policies.password",
"authentik.policies.reputation", "authentik.policies.reputation",
"authentik.policies", "authentik.policies",
@ -4047,6 +4088,7 @@
"authentik_policies_event_matcher.eventmatcherpolicy", "authentik_policies_event_matcher.eventmatcherpolicy",
"authentik_policies_expiry.passwordexpirypolicy", "authentik_policies_expiry.passwordexpirypolicy",
"authentik_policies_expression.expressionpolicy", "authentik_policies_expression.expressionpolicy",
"authentik_policies_geoip.geoippolicy",
"authentik_policies_password.passwordpolicy", "authentik_policies_password.passwordpolicy",
"authentik_policies_reputation.reputationpolicy", "authentik_policies_reputation.reputationpolicy",
"authentik_policies.policybinding", "authentik_policies.policybinding",
@ -4250,6 +4292,318 @@
} }
} }
}, },
"model_authentik_policies_geoip.geoippolicy": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"execution_logging": {
"type": "boolean",
"title": "Execution logging",
"description": "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
},
"asns": {
"type": "array",
"items": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Asns"
},
"title": "Asns"
},
"countries": {
"type": "array",
"items": {
"type": "string",
"enum": [
"",
"AF",
"AX",
"AL",
"DZ",
"AS",
"AD",
"AO",
"AI",
"AQ",
"AG",
"AR",
"AM",
"AW",
"AU",
"AT",
"AZ",
"BS",
"BH",
"BD",
"BB",
"BY",
"BE",
"BZ",
"BJ",
"BM",
"BT",
"BO",
"BQ",
"BA",
"BW",
"BV",
"BR",
"IO",
"BN",
"BG",
"BF",
"BI",
"CV",
"KH",
"CM",
"CA",
"KY",
"CF",
"TD",
"CL",
"CN",
"CX",
"CC",
"CO",
"KM",
"CG",
"CD",
"CK",
"CR",
"CI",
"HR",
"CU",
"CW",
"CY",
"CZ",
"DK",
"DJ",
"DM",
"DO",
"EC",
"EG",
"SV",
"GQ",
"ER",
"EE",
"SZ",
"ET",
"FK",
"FO",
"FJ",
"FI",
"FR",
"GF",
"PF",
"TF",
"GA",
"GM",
"GE",
"DE",
"GH",
"GI",
"GR",
"GL",
"GD",
"GP",
"GU",
"GT",
"GG",
"GN",
"GW",
"GY",
"HT",
"HM",
"VA",
"HN",
"HK",
"HU",
"IS",
"IN",
"ID",
"IR",
"IQ",
"IE",
"IM",
"IL",
"IT",
"JM",
"JP",
"JE",
"JO",
"KZ",
"KE",
"KI",
"KW",
"KG",
"LA",
"LV",
"LB",
"LS",
"LR",
"LY",
"LI",
"LT",
"LU",
"MO",
"MG",
"MW",
"MY",
"MV",
"ML",
"MT",
"MH",
"MQ",
"MR",
"MU",
"YT",
"MX",
"FM",
"MD",
"MC",
"MN",
"ME",
"MS",
"MA",
"MZ",
"MM",
"NA",
"NR",
"NP",
"NL",
"NC",
"NZ",
"NI",
"NE",
"NG",
"NU",
"NF",
"KP",
"MK",
"MP",
"NO",
"OM",
"PK",
"PW",
"PS",
"PA",
"PG",
"PY",
"PE",
"PH",
"PN",
"PL",
"PT",
"PR",
"QA",
"RE",
"RO",
"RU",
"RW",
"BL",
"SH",
"KN",
"LC",
"MF",
"PM",
"VC",
"WS",
"SM",
"ST",
"SA",
"SN",
"RS",
"SC",
"SL",
"SG",
"SX",
"SK",
"SI",
"SB",
"SO",
"ZA",
"GS",
"KR",
"SS",
"ES",
"LK",
"SD",
"SR",
"SJ",
"SE",
"CH",
"SY",
"TW",
"TJ",
"TZ",
"TH",
"TL",
"TG",
"TK",
"TO",
"TT",
"TN",
"TR",
"TM",
"TC",
"TV",
"UG",
"UA",
"AE",
"GB",
"UM",
"US",
"UY",
"UZ",
"VU",
"VE",
"VN",
"VG",
"VI",
"WF",
"EH",
"YE",
"ZM",
"ZW"
]
},
"maxItems": 249,
"title": "Countries"
}
},
"required": []
},
"model_authentik_policies_geoip.geoippolicy_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_geoippolicy",
"change_geoippolicy",
"delete_geoippolicy",
"view_geoippolicy"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_policies_password.passwordpolicy": { "model_authentik_policies_password.passwordpolicy": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5579,6 +5933,10 @@
"authentik_policies_expression.change_expressionpolicy", "authentik_policies_expression.change_expressionpolicy",
"authentik_policies_expression.delete_expressionpolicy", "authentik_policies_expression.delete_expressionpolicy",
"authentik_policies_expression.view_expressionpolicy", "authentik_policies_expression.view_expressionpolicy",
"authentik_policies_geoip.add_geoippolicy",
"authentik_policies_geoip.change_geoippolicy",
"authentik_policies_geoip.delete_geoippolicy",
"authentik_policies_geoip.view_geoippolicy",
"authentik_policies_password.add_passwordpolicy", "authentik_policies_password.add_passwordpolicy",
"authentik_policies_password.change_passwordpolicy", "authentik_policies_password.change_passwordpolicy",
"authentik_policies_password.delete_passwordpolicy", "authentik_policies_password.delete_passwordpolicy",
@ -10903,6 +11261,10 @@
"authentik_policies_expression.change_expressionpolicy", "authentik_policies_expression.change_expressionpolicy",
"authentik_policies_expression.delete_expressionpolicy", "authentik_policies_expression.delete_expressionpolicy",
"authentik_policies_expression.view_expressionpolicy", "authentik_policies_expression.view_expressionpolicy",
"authentik_policies_geoip.add_geoippolicy",
"authentik_policies_geoip.change_geoippolicy",
"authentik_policies_geoip.delete_geoippolicy",
"authentik_policies_geoip.view_geoippolicy",
"authentik_policies_password.add_passwordpolicy", "authentik_policies_password.add_passwordpolicy",
"authentik_policies_password.change_passwordpolicy", "authentik_policies_password.change_passwordpolicy",
"authentik_policies_password.delete_passwordpolicy", "authentik_policies_password.delete_passwordpolicy",

23
poetry.lock generated
View File

@ -1210,6 +1210,27 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"] argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"] bcrypt = ["bcrypt"]
[[package]]
name = "django-countries"
version = "7.6.1"
description = "Provides a country field for Django models."
optional = false
python-versions = "*"
files = [
{file = "django-countries-7.6.1.tar.gz", hash = "sha256:c772d4e3e54afcc5f97a018544e96f246c6d9f1db51898ab0c15cd57e19437cf"},
{file = "django_countries-7.6.1-py3-none-any.whl", hash = "sha256:1ed20842fe0f6194f91faca21076649513846a8787c9eb5aeec3cbe1656b8acc"},
]
[package.dependencies]
asgiref = "*"
typing-extensions = "*"
[package.extras]
dev = ["black", "django", "djangorestframework", "graphene-django", "pytest", "pytest-django", "tox (==4.*)"]
maintainer = ["django", "zest.releaser[recommended]"]
pyuca = ["pyuca"]
test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytest-django"]
[[package]] [[package]]
name = "django-cte" name = "django-cte"
version = "1.3.3" version = "1.3.3"
@ -5463,4 +5484,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "~3.12" python-versions = "~3.12"
content-hash = "1147e0dceb83f7c487e6b4d96270bad644e6618585e00fa2a1fc89cbf2efe300" content-hash = "ef49ce543812a47597b9108ca277cd4b6563fe00d0739e763b6e1e1151c95eba"

View File

@ -92,6 +92,7 @@ dacite = "*"
deepmerge = "*" deepmerge = "*"
defusedxml = "*" defusedxml = "*"
django = "*" django = "*"
django-countries = "*"
django-cte = "*" django-cte = "*"
django-filter = "*" django-filter = "*"
django-guardian = "*" django-guardian = "*"

View File

@ -12075,6 +12075,305 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/policies/geoip/:
get:
operationId: policies_geoip_list
description: GeoIP Viewset
parameters:
- in: query
name: name
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- policies
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedGeoIPPolicyList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: policies_geoip_create
description: GeoIP Viewset
tags:
- policies
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GeoIPPolicyRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/GeoIPPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/policies/geoip/{policy_uuid}/:
get:
operationId: policies_geoip_retrieve
description: GeoIP Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this GeoIP Policy.
required: true
tags:
- policies
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GeoIPPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: policies_geoip_update
description: GeoIP Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this GeoIP Policy.
required: true
tags:
- policies
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GeoIPPolicyRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GeoIPPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: policies_geoip_partial_update
description: GeoIP Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this GeoIP Policy.
required: true
tags:
- policies
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGeoIPPolicyRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GeoIPPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: policies_geoip_destroy
description: GeoIP Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this GeoIP Policy.
required: true
tags:
- policies
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/policies/geoip/{policy_uuid}/used_by/:
get:
operationId: policies_geoip_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this GeoIP Policy.
required: true
tags:
- policies
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/policies/geoip_iso3166/:
get:
operationId: policies_geoip_iso3166_list
description: Get all countries in ISO-3166-1
tags:
- policies
security:
- authentik: []
- {}
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/DetailedCountry'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/policies/password/: /policies/password/:
get: get:
operationId: policies_password_list operationId: policies_password_list
@ -21287,6 +21586,7 @@ paths:
- authentik_policies_event_matcher.eventmatcherpolicy - authentik_policies_event_matcher.eventmatcherpolicy
- authentik_policies_expiry.passwordexpirypolicy - authentik_policies_expiry.passwordexpirypolicy
- authentik_policies_expression.expressionpolicy - authentik_policies_expression.expressionpolicy
- authentik_policies_geoip.geoippolicy
- authentik_policies_password.passwordpolicy - authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy - authentik_policies_reputation.reputationpolicy
- authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovider
@ -21514,6 +21814,7 @@ paths:
- authentik_policies_event_matcher.eventmatcherpolicy - authentik_policies_event_matcher.eventmatcherpolicy
- authentik_policies_expiry.passwordexpirypolicy - authentik_policies_expiry.passwordexpirypolicy
- authentik_policies_expression.expressionpolicy - authentik_policies_expression.expressionpolicy
- authentik_policies_geoip.geoippolicy
- authentik_policies_password.passwordpolicy - authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy - authentik_policies_reputation.reputationpolicy
- authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovider
@ -33705,6 +34006,7 @@ components:
- authentik.policies.event_matcher - authentik.policies.event_matcher
- authentik.policies.expiry - authentik.policies.expiry
- authentik.policies.expression - authentik.policies.expression
- authentik.policies.geoip
- authentik.policies.password - authentik.policies.password
- authentik.policies.reputation - authentik.policies.reputation
- authentik.policies - authentik.policies
@ -35742,6 +36044,258 @@ components:
required: required:
- x_cord - x_cord
- y_cord - y_cord
CountryCodeEnum:
enum:
- AF
- AX
- AL
- DZ
- AS
- AD
- AO
- AI
- AQ
- AG
- AR
- AM
- AW
- AU
- AT
- AZ
- BS
- BH
- BD
- BB
- BY
- BE
- BZ
- BJ
- BM
- BT
- BO
- BQ
- BA
- BW
- BV
- BR
- IO
- BN
- BG
- BF
- BI
- CV
- KH
- CM
- CA
- KY
- CF
- TD
- CL
- CN
- CX
- CC
- CO
- KM
- CG
- CD
- CK
- CR
- CI
- HR
- CU
- CW
- CY
- CZ
- DK
- DJ
- DM
- DO
- EC
- EG
- SV
- GQ
- ER
- EE
- SZ
- ET
- FK
- FO
- FJ
- FI
- FR
- GF
- PF
- TF
- GA
- GM
- GE
- DE
- GH
- GI
- GR
- GL
- GD
- GP
- GU
- GT
- GG
- GN
- GW
- GY
- HT
- HM
- VA
- HN
- HK
- HU
- IS
- IN
- ID
- IR
- IQ
- IE
- IM
- IL
- IT
- JM
- JP
- JE
- JO
- KZ
- KE
- KI
- KW
- KG
- LA
- LV
- LB
- LS
- LR
- LY
- LI
- LT
- LU
- MO
- MG
- MW
- MY
- MV
- ML
- MT
- MH
- MQ
- MR
- MU
- YT
- MX
- FM
- MD
- MC
- MN
- ME
- MS
- MA
- MZ
- MM
- NA
- NR
- NP
- NL
- NC
- NZ
- NI
- NE
- NG
- NU
- NF
- KP
- MK
- MP
- 'NO'
- OM
- PK
- PW
- PS
- PA
- PG
- PY
- PE
- PH
- PN
- PL
- PT
- PR
- QA
- RE
- RO
- RU
- RW
- BL
- SH
- KN
- LC
- MF
- PM
- VC
- WS
- SM
- ST
- SA
- SN
- RS
- SC
- SL
- SG
- SX
- SK
- SI
- SB
- SO
- ZA
- GS
- KR
- SS
- ES
- LK
- SD
- SR
- SJ
- SE
- CH
- SY
- TW
- TJ
- TZ
- TH
- TL
- TG
- TK
- TO
- TT
- TN
- TR
- TM
- TC
- TV
- UG
- UA
- AE
- GB
- UM
- US
- UY
- UZ
- VU
- VE
- VN
- VG
- VI
- WF
- EH
- YE
- ZM
- ZW
type: string
CurrentBrand: CurrentBrand:
type: object type: object
description: Partial brand information for styling description: Partial brand information for styling
@ -35848,6 +36402,37 @@ components:
type: string type: string
required: required:
- name - name
DetailedCountry:
type: object
properties:
code:
$ref: '#/components/schemas/CountryCodeEnum'
name:
type: string
required:
- code
- name
DetailedCountryField:
type: object
properties:
code:
$ref: '#/components/schemas/CountryCodeEnum'
name:
type: string
required:
- code
- name
DetailedCountryFieldRequest:
type: object
properties:
code:
$ref: '#/components/schemas/CountryCodeEnum'
name:
type: string
minLength: 1
required:
- code
- name
Device: Device:
type: object type: object
description: Serializer for Duo authenticator devices description: Serializer for Duo authenticator devices
@ -37441,6 +38026,92 @@ components:
type: string type: string
required: required:
- detail - detail
GeoIPPolicy:
type: object
description: GeoIP Policy Serializer
properties:
pk:
type: string
format: uuid
readOnly: true
title: Policy uuid
name:
type: string
execution_logging:
type: boolean
description: When this option is enabled, all executions of this policy
will be logged. By default, only execution errors are logged.
component:
type: string
description: Get object component so that we know how to edit the object
readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
readOnly: true
verbose_name_plural:
type: string
description: Return object's plural verbose_name
readOnly: true
meta_model_name:
type: string
description: Return internal model name
readOnly: true
bound_to:
type: integer
description: Return objects policy is bound to
readOnly: true
asns:
type: array
items:
type: integer
maximum: 2147483647
minimum: -2147483648
countries:
type: array
items:
$ref: '#/components/schemas/CountryCodeEnum'
maxItems: 249
countries_obj:
type: array
items:
$ref: '#/components/schemas/DetailedCountryField'
readOnly: true
required:
- bound_to
- component
- countries
- countries_obj
- meta_model_name
- name
- pk
- verbose_name
- verbose_name_plural
GeoIPPolicyRequest:
type: object
description: GeoIP Policy Serializer
properties:
name:
type: string
minLength: 1
execution_logging:
type: boolean
description: When this option is enabled, all executions of this policy
will be logged. By default, only execution errors are logged.
asns:
type: array
items:
type: integer
maximum: 2147483647
minimum: -2147483648
countries:
type: array
items:
$ref: '#/components/schemas/CountryCodeEnum'
maxItems: 249
required:
- countries
- name
GeoipBindingEnum: GeoipBindingEnum:
enum: enum:
- no_binding - no_binding
@ -39405,6 +40076,7 @@ components:
- authentik_policies_event_matcher.eventmatcherpolicy - authentik_policies_event_matcher.eventmatcherpolicy
- authentik_policies_expiry.passwordexpirypolicy - authentik_policies_expiry.passwordexpirypolicy
- authentik_policies_expression.expressionpolicy - authentik_policies_expression.expressionpolicy
- authentik_policies_geoip.geoippolicy
- authentik_policies_password.passwordpolicy - authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy - authentik_policies_reputation.reputationpolicy
- authentik_policies.policybinding - authentik_policies.policybinding
@ -40790,6 +41462,18 @@ components:
required: required:
- pagination - pagination
- results - results
PaginatedGeoIPPolicyList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/GeoIPPolicy'
required:
- pagination
- results
PaginatedGoogleWorkspaceProviderGroupList: PaginatedGoogleWorkspaceProviderGroupList:
type: object type: object
properties: properties:
@ -42894,6 +43578,28 @@ components:
to a challenge. RETRY returns the error message and a similar challenge to a challenge. RETRY returns the error message and a similar challenge
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
restarts the flow while keeping the current context. restarts the flow while keeping the current context.
PatchedGeoIPPolicyRequest:
type: object
description: GeoIP Policy Serializer
properties:
name:
type: string
minLength: 1
execution_logging:
type: boolean
description: When this option is enabled, all executions of this policy
will be logged. By default, only execution errors are logged.
asns:
type: array
items:
type: integer
maximum: 2147483647
minimum: -2147483648
countries:
type: array
items:
$ref: '#/components/schemas/CountryCodeEnum'
maxItems: 249
PatchedGoogleWorkspaceProviderMappingRequest: PatchedGoogleWorkspaceProviderMappingRequest:
type: object type: object
description: GoogleWorkspaceProviderMapping Serializer description: GoogleWorkspaceProviderMapping Serializer

View File

@ -3,6 +3,7 @@ import "@goauthentik/admin/policies/dummy/DummyPolicyForm";
import "@goauthentik/admin/policies/event_matcher/EventMatcherPolicyForm"; import "@goauthentik/admin/policies/event_matcher/EventMatcherPolicyForm";
import "@goauthentik/admin/policies/expiry/ExpiryPolicyForm"; import "@goauthentik/admin/policies/expiry/ExpiryPolicyForm";
import "@goauthentik/admin/policies/expression/ExpressionPolicyForm"; import "@goauthentik/admin/policies/expression/ExpressionPolicyForm";
import "@goauthentik/admin/policies/geoip/GeoIPPolicyForm";
import "@goauthentik/admin/policies/password/PasswordPolicyForm"; import "@goauthentik/admin/policies/password/PasswordPolicyForm";
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";

View File

@ -0,0 +1,35 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DetailedCountry, PoliciesApi } from "@goauthentik/api";
class CountryCache {
countries: DetailedCountry[];
lastReceivedAt?: number;
TTL: number;
constructor() {
this.countries = [];
this.lastReceivedAt = undefined;
// 1 minute
this.TTL = 60 * 1000;
}
async getCountries() {
const shouldInvalidate =
this.lastReceivedAt === undefined ||
new Date().getTime() - this.lastReceivedAt > this.TTL;
if (!shouldInvalidate) {
return this.countries;
}
await new PoliciesApi(DEFAULT_CONFIG).policiesGeoipIso3166List().then((response) => {
this.countries = response;
this.lastReceivedAt = new Date().getTime();
});
return this.countries;
}
}
export const countryCache = new CountryCache();

View File

@ -0,0 +1,125 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/ak-dual-select";
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { DetailedCountry, GeoIPPolicy, PoliciesApi } from "@goauthentik/api";
import { countryCache } from "./CountryCache";
function countryToPair(country: DetailedCountry): DualSelectPair {
return [country.code, country.name];
}
@customElement("ak-policy-geoip-form")
export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
loadInstance(pk: string): Promise<GeoIPPolicy> {
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipRetrieve({
policyUuid: pk,
});
}
async send(data: GeoIPPolicy): Promise<GeoIPPolicy> {
if (data.asns?.toString() === "") {
data.asns = [];
} else {
data.asns = (data.asns as unknown as string).split(",").map(Number);
}
if (this.instance) {
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipUpdate({
policyUuid: this.instance.pk || "",
geoIPPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipCreate({
geoIPPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>
${msg(
"Ensure the user satisfies requirements of geography or network topology, based on IP address. If any of the configured values match, the policy passes.",
)}
</span>
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="executionLogging">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Execution logging")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("ASNs")} name="asns">
<input
type="text"
value="${this.instance?.asns ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg(
"List of autonomous system numbers. Comma separated. E.g. 13335, 15169, 20940",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Countries")} name="countries">
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return countryCache
.getCountries()
.then((results) => {
if (!search) return results;
return results.filter((result) =>
result.name
.toLowerCase()
.includes(search.toLowerCase()),
);
})
.then((results) => {
return {
options: results.map(countryToPair),
};
});
}}
.selected=${(this.instance?.countriesObj ?? []).map(countryToPair)}
available-label="${msg("Available Countries")}"
selected-label="${msg("Selected Countries")}"
>
</ak-dual-select-provider>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}
}

View File

@ -12,7 +12,7 @@ export type BasePagination = Pick<
>; >;
export type DataProvision = { export type DataProvision = {
pagination: Pagination; pagination?: Pagination;
options: DualSelectPair[]; options: DualSelectPair[];
}; };

View File

@ -1,6 +1,6 @@
# GeoIP # GeoIP
authentik supports GeoIP to add additional information to login/authorization/enrollment requests, and make policy decisions based on the lookup result. authentik supports GeoIP to add additional information to login/authorization/enrollment requests. Additionally, a [GeoIP policy](../policies/#geoip-policy) can be used to make policy decisions based on the lookup result.
### Configuration ### Configuration

View File

@ -55,6 +55,10 @@ import Objects from "../expressions/_objects.md";
- `geoip`: GeoIP dictionary. The following fields are available: - `geoip`: GeoIP dictionary. The following fields are available:
:::info
For basic country matching, consider using a [GeoIP policy](index.md#geoip-policy).
:::
- `continent`: a two character continent code like `NA` (North America) or `OC` (Oceania). - `continent`: a two character continent code like `NA` (North America) or `OC` (Oceania).
- `country`: the two character [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) alpha code for the country. - `country`: the two character [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) alpha code for the country.
- `lat`: the approximate latitude of the location associated with the IP address. - `lat`: the approximate latitude of the location associated with the IP address.
@ -62,11 +66,15 @@ import Objects from "../expressions/_objects.md";
- `city`: the name of the city. May be empty. - `city`: the name of the city. May be empty.
```python ```python
return context["geoip"]["country"] == "US" return context["geoip"]["continent"] == "EU"
``` ```
- `asn`: ASN dictionary. The follow fields are available: - `asn`: ASN dictionary. The follow fields are available:
:::info
For basic ASN matching, consider using a [GeoIP policy](index.md#geoip-policy).
:::
- `asn`: the autonomous system number associated with the IP address. - `asn`: the autonomous system number associated with the IP address.
- `as_org`: the organization associated with the registered autonomous system number for the IP address. - `as_org`: the organization associated with the registered autonomous system number for the IP address.
- `network`: the network associated with the record. In particular, this is the largest network where all of the fields except `ip_address` have the same value. - `network`: the network associated with the record. In particular, this is the largest network where all of the fields except `ip_address` have the same value.

View File

@ -6,22 +6,26 @@ title: Policies
This policy is used by the events subsystem. You can use this policy to match events by multiple different criteria, to choose when you get notified. This policy is used by the events subsystem. You can use this policy to match events by multiple different criteria, to choose when you get notified.
## Expression Policy ## Expression policy
See [Expression Policy](expression.mdx). See [Expression policy](expression.mdx).
## Have I Been Pwned Policy ## GeoIP policy
Use this policy for simple GeoIP lookups, such as country or ASN matching. (For a more advanced GeoIP lookup, use an [Expression policy](expression.mdx).)
## Have I Been Pwned policy
:::info :::info
This policy is deprecated since authentik 2022.11.0, as this can be done with the password policy now. This policy is deprecated since authentik 2022.11.0, as this can be done with the password policy now.
::: :::
This policy checks the hashed password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within authentik. This policy checks the hashed password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within authentik.
## Password-Expiry Policy ## Password-Expiry policy
This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password. This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password.
## Password Policy ## Password policy
This policy allows you to specify password rules, such as length and required characters. This policy allows you to specify password rules, such as length and required characters.
The following rules can be set: The following rules can be set:
@ -37,7 +41,7 @@ Starting with authentik 2022.11.0, the following checks can also be done with th
- Check the password hash against the database of [Have I Been Pwned](https://haveibeenpwned.com/). Only the first 5 characters of the hashed password are transmitted, the rest is compared in authentik - Check the password hash against the database of [Have I Been Pwned](https://haveibeenpwned.com/). Only the first 5 characters of the hashed password are transmitted, the rest is compared in authentik
- Check the password against the password complexity checker [zxcvbn](https://github.com/dropbox/zxcvbn), which detects weak password on various metrics. - Check the password against the password complexity checker [zxcvbn](https://github.com/dropbox/zxcvbn), which detects weak password on various metrics.
## Reputation Policy ## Reputation policy
authentik keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one). authentik keeps track of failed login attempts by source IP and attempted username. These values are saved as scores. Each failed login decreases the score for the client IP as well as the targeted username by 1 (one).