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:
0
authentik/policies/geoip/__init__.py
Normal file
0
authentik/policies/geoip/__init__.py
Normal file
55
authentik/policies/geoip/api.py
Normal file
55
authentik/policies/geoip/api.py
Normal 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"]
|
11
authentik/policies/geoip/apps.py
Normal file
11
authentik/policies/geoip/apps.py
Normal 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"
|
5
authentik/policies/geoip/exceptions.py
Normal file
5
authentik/policies/geoip/exceptions.py
Normal 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"""
|
52
authentik/policies/geoip/migrations/0001_initial.py
Normal file
52
authentik/policies/geoip/migrations/0001_initial.py
Normal 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",),
|
||||
),
|
||||
]
|
0
authentik/policies/geoip/migrations/__init__.py
Normal file
0
authentik/policies/geoip/migrations/__init__.py
Normal file
92
authentik/policies/geoip/models.py
Normal file
92
authentik/policies/geoip/models.py
Normal 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")
|
21
authentik/policies/geoip/serializer_fields.py
Normal file
21
authentik/policies/geoip/serializer_fields.py
Normal 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)
|
128
authentik/policies/geoip/tests.py
Normal file
128
authentik/policies/geoip/tests.py
Normal 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)
|
10
authentik/policies/geoip/urls.py
Normal file
10
authentik/policies/geoip/urls.py
Normal 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"),
|
||||
]
|
@ -59,6 +59,7 @@ SHARED_APPS = [
|
||||
"django_filters",
|
||||
"drf_spectacular",
|
||||
"django_prometheus",
|
||||
"django_countries",
|
||||
"pgactivity",
|
||||
"pglock",
|
||||
"channels",
|
||||
@ -76,6 +77,7 @@ TENANT_APPS = [
|
||||
"authentik.policies.event_matcher",
|
||||
"authentik.policies.expiry",
|
||||
"authentik.policies.expression",
|
||||
"authentik.policies.geoip",
|
||||
"authentik.policies.password",
|
||||
"authentik.policies.reputation",
|
||||
"authentik.policies",
|
||||
@ -146,6 +148,7 @@ SPECTACULAR_SETTINGS = {
|
||||
"url": "https://github.com/goauthentik/authentik/blob/main/LICENSE",
|
||||
},
|
||||
"ENUM_NAME_OVERRIDES": {
|
||||
"CountryCodeEnum": "django_countries.countries",
|
||||
"EventActions": "authentik.events.models.EventAction",
|
||||
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
|
||||
"FlowLayoutEnum": "authentik.flows.models.FlowLayout",
|
||||
|
@ -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",
|
||||
"required": [
|
||||
@ -3979,6 +4019,7 @@
|
||||
"authentik.policies.event_matcher",
|
||||
"authentik.policies.expiry",
|
||||
"authentik.policies.expression",
|
||||
"authentik.policies.geoip",
|
||||
"authentik.policies.password",
|
||||
"authentik.policies.reputation",
|
||||
"authentik.policies",
|
||||
@ -4047,6 +4088,7 @@
|
||||
"authentik_policies_event_matcher.eventmatcherpolicy",
|
||||
"authentik_policies_expiry.passwordexpirypolicy",
|
||||
"authentik_policies_expression.expressionpolicy",
|
||||
"authentik_policies_geoip.geoippolicy",
|
||||
"authentik_policies_password.passwordpolicy",
|
||||
"authentik_policies_reputation.reputationpolicy",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -5579,6 +5933,10 @@
|
||||
"authentik_policies_expression.change_expressionpolicy",
|
||||
"authentik_policies_expression.delete_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.change_passwordpolicy",
|
||||
"authentik_policies_password.delete_passwordpolicy",
|
||||
@ -10903,6 +11261,10 @@
|
||||
"authentik_policies_expression.change_expressionpolicy",
|
||||
"authentik_policies_expression.delete_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.change_passwordpolicy",
|
||||
"authentik_policies_password.delete_passwordpolicy",
|
||||
|
23
poetry.lock
generated
23
poetry.lock
generated
@ -1210,6 +1210,27 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||
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]]
|
||||
name = "django-cte"
|
||||
version = "1.3.3"
|
||||
@ -5463,4 +5484,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.12"
|
||||
content-hash = "1147e0dceb83f7c487e6b4d96270bad644e6618585e00fa2a1fc89cbf2efe300"
|
||||
content-hash = "ef49ce543812a47597b9108ca277cd4b6563fe00d0739e763b6e1e1151c95eba"
|
||||
|
@ -92,6 +92,7 @@ dacite = "*"
|
||||
deepmerge = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
django-countries = "*"
|
||||
django-cte = "*"
|
||||
django-filter = "*"
|
||||
django-guardian = "*"
|
||||
|
706
schema.yml
706
schema.yml
@ -12075,6 +12075,305 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
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/:
|
||||
get:
|
||||
operationId: policies_password_list
|
||||
@ -21287,6 +21586,7 @@ paths:
|
||||
- authentik_policies_event_matcher.eventmatcherpolicy
|
||||
- authentik_policies_expiry.passwordexpirypolicy
|
||||
- authentik_policies_expression.expressionpolicy
|
||||
- authentik_policies_geoip.geoippolicy
|
||||
- authentik_policies_password.passwordpolicy
|
||||
- authentik_policies_reputation.reputationpolicy
|
||||
- authentik_providers_google_workspace.googleworkspaceprovider
|
||||
@ -21514,6 +21814,7 @@ paths:
|
||||
- authentik_policies_event_matcher.eventmatcherpolicy
|
||||
- authentik_policies_expiry.passwordexpirypolicy
|
||||
- authentik_policies_expression.expressionpolicy
|
||||
- authentik_policies_geoip.geoippolicy
|
||||
- authentik_policies_password.passwordpolicy
|
||||
- authentik_policies_reputation.reputationpolicy
|
||||
- authentik_providers_google_workspace.googleworkspaceprovider
|
||||
@ -33705,6 +34006,7 @@ components:
|
||||
- authentik.policies.event_matcher
|
||||
- authentik.policies.expiry
|
||||
- authentik.policies.expression
|
||||
- authentik.policies.geoip
|
||||
- authentik.policies.password
|
||||
- authentik.policies.reputation
|
||||
- authentik.policies
|
||||
@ -35742,6 +36044,258 @@ components:
|
||||
required:
|
||||
- x_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:
|
||||
type: object
|
||||
description: Partial brand information for styling
|
||||
@ -35848,6 +36402,37 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- 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:
|
||||
type: object
|
||||
description: Serializer for Duo authenticator devices
|
||||
@ -37441,6 +38026,92 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- 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:
|
||||
enum:
|
||||
- no_binding
|
||||
@ -39405,6 +40076,7 @@ components:
|
||||
- authentik_policies_event_matcher.eventmatcherpolicy
|
||||
- authentik_policies_expiry.passwordexpirypolicy
|
||||
- authentik_policies_expression.expressionpolicy
|
||||
- authentik_policies_geoip.geoippolicy
|
||||
- authentik_policies_password.passwordpolicy
|
||||
- authentik_policies_reputation.reputationpolicy
|
||||
- authentik_policies.policybinding
|
||||
@ -40790,6 +41462,18 @@ components:
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedGeoIPPolicyList:
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GeoIPPolicy'
|
||||
required:
|
||||
- pagination
|
||||
- results
|
||||
PaginatedGoogleWorkspaceProviderGroupList:
|
||||
type: object
|
||||
properties:
|
||||
@ -42894,6 +43578,28 @@ components:
|
||||
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
|
||||
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:
|
||||
type: object
|
||||
description: GoogleWorkspaceProviderMapping Serializer
|
||||
|
@ -3,6 +3,7 @@ import "@goauthentik/admin/policies/dummy/DummyPolicyForm";
|
||||
import "@goauthentik/admin/policies/event_matcher/EventMatcherPolicyForm";
|
||||
import "@goauthentik/admin/policies/expiry/ExpiryPolicyForm";
|
||||
import "@goauthentik/admin/policies/expression/ExpressionPolicyForm";
|
||||
import "@goauthentik/admin/policies/geoip/GeoIPPolicyForm";
|
||||
import "@goauthentik/admin/policies/password/PasswordPolicyForm";
|
||||
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
|
35
web/src/admin/policies/geoip/CountryCache.ts
Normal file
35
web/src/admin/policies/geoip/CountryCache.ts
Normal 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();
|
125
web/src/admin/policies/geoip/GeoIPPolicyForm.ts
Normal file
125
web/src/admin/policies/geoip/GeoIPPolicyForm.ts
Normal 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>`;
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ export type BasePagination = Pick<
|
||||
>;
|
||||
|
||||
export type DataProvision = {
|
||||
pagination: Pagination;
|
||||
pagination?: Pagination;
|
||||
options: DualSelectPair[];
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
|
@ -55,6 +55,10 @@ import Objects from "../expressions/_objects.md";
|
||||
|
||||
- `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).
|
||||
- `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.
|
||||
@ -62,11 +66,15 @@ import Objects from "../expressions/_objects.md";
|
||||
- `city`: the name of the city. May be empty.
|
||||
|
||||
```python
|
||||
return context["geoip"]["country"] == "US"
|
||||
return context["geoip"]["continent"] == "EU"
|
||||
```
|
||||
|
||||
- `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.
|
||||
- `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.
|
||||
|
@ -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.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Password Policy
|
||||
## Password policy
|
||||
|
||||
This policy allows you to specify password rules, such as length and required characters.
|
||||
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 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).
|
||||
|
||||
|
Reference in New Issue
Block a user