core: Add `resolve_dns` and `reverse_dns` functions to evaluator (#4769)
* Add resolve_dns * Add reverse_dns * Fix lint * add caching, small optimisation Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Added time-aware LRU cache --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
2eb7c16a9a
commit
a6eba37d5a
|
@ -1,9 +1,11 @@
|
||||||
"""authentik expression policy evaluator"""
|
"""authentik expression policy evaluator"""
|
||||||
import re
|
import re
|
||||||
|
import socket
|
||||||
from ipaddress import ip_address, ip_network
|
from ipaddress import ip_address, ip_network
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
from typing import Any, Iterable, Optional
|
from typing import Any, Iterable, Optional
|
||||||
|
|
||||||
|
from cachetools import TLRUCache, cached
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django_otp import devices_for_user
|
from django_otp import devices_for_user
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
@ -41,6 +43,8 @@ class BaseEvaluator:
|
||||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||||
"ak_user_by": BaseEvaluator.expr_user_by,
|
"ak_user_by": BaseEvaluator.expr_user_by,
|
||||||
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
|
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
|
||||||
|
"resolve_dns": BaseEvaluator.expr_resolve_dns,
|
||||||
|
"reverse_dns": BaseEvaluator.expr_reverse_dns,
|
||||||
"ak_create_event": self.expr_event_create,
|
"ak_create_event": self.expr_event_create,
|
||||||
"ak_logger": get_logger(self._filename).bind(),
|
"ak_logger": get_logger(self._filename).bind(),
|
||||||
"requests": get_http_session(),
|
"requests": get_http_session(),
|
||||||
|
@ -49,6 +53,39 @@ class BaseEvaluator:
|
||||||
}
|
}
|
||||||
self._context = {}
|
self._context = {}
|
||||||
|
|
||||||
|
@cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
|
||||||
|
@staticmethod
|
||||||
|
def expr_resolve_dns(host: str, ip_version: Optional[int] = None) -> list[str]:
|
||||||
|
"""Resolve host to a list of IPv4 and/or IPv6 addresses."""
|
||||||
|
# Although it seems to be fine (raising OSError), docs warn
|
||||||
|
# against passing `None` for both the host and the port
|
||||||
|
# https://docs.python.org/3/library/socket.html#socket.getaddrinfo
|
||||||
|
host = host or ""
|
||||||
|
|
||||||
|
ip_list = []
|
||||||
|
|
||||||
|
family = 0
|
||||||
|
if ip_version == 4:
|
||||||
|
family = socket.AF_INET
|
||||||
|
if ip_version == 6:
|
||||||
|
family = socket.AF_INET6
|
||||||
|
|
||||||
|
try:
|
||||||
|
for ip_addr in socket.getaddrinfo(host, None, family=family):
|
||||||
|
ip_list.append(str(ip_addr[4][0]))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return list(set(ip_list))
|
||||||
|
|
||||||
|
@cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
|
||||||
|
@staticmethod
|
||||||
|
def expr_reverse_dns(ip_addr: str) -> str:
|
||||||
|
"""Perform a reverse DNS lookup."""
|
||||||
|
try:
|
||||||
|
return socket.getfqdn(ip_addr)
|
||||||
|
except OSError:
|
||||||
|
return ip_addr
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
|
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
|
||||||
"""Flatten `value` if its a list"""
|
"""Flatten `value` if its a list"""
|
||||||
|
|
|
@ -100,3 +100,30 @@ You can also check if an IP Address is within a subnet by writing the following:
|
||||||
ip_address('192.0.2.1') in ip_network('192.0.2.0/24')
|
ip_address('192.0.2.1') in ip_network('192.0.2.0/24')
|
||||||
# evaluates to True
|
# evaluates to True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## DNS resolution and reverse DNS lookups
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Requires authentik 2023.3 or higher
|
||||||
|
:::
|
||||||
|
|
||||||
|
To resolve a hostname to a list of IP addresses, use the functions `resolve_dns(hostname)` and `resolve_dns(hostname, ip_version)`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
resolve_dns("google.com") # return a list of all IPv4 and IPv6 addresses
|
||||||
|
resolve_dns("google.com", 4) # return a list of only IP4 addresses
|
||||||
|
resolve_dns("google.com", 6) # return a list of only IP6 addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also do reverse DNS lookups.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Reverse DNS lookups may not return the expected host if the IP address is part of a shared hosting environment.
|
||||||
|
See: https://stackoverflow.com/a/19867936
|
||||||
|
:::
|
||||||
|
|
||||||
|
To perform a reverse DNS lookup use `reverse_dns("192.0.2.0")`. If no DNS records are found the original IP address is returned.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
DNS resolving results are cached in memory. The last 32 unique queries are cached for up to 3 minutes.
|
||||||
|
:::
|
||||||
|
|
Reference in New Issue