8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-23 15:33:45 +00:00

documentation

This commit is contained in:
chapeau 2020-11-29 10:33:40 +01:00
parent 048140db20
commit fdb93e1759
4 changed files with 204 additions and 129 deletions

View file

@ -48,25 +48,12 @@ import radiusd
from requests import HTTPError
import urllib.parse
path = (os.path.dirname(os.path.abspath(__file__)))
config = ConfigParser()
config.read(path+'/config.ini')
api_hostname = config.get('Re2o', 'hostname')
api_password = config.get('Re2o', 'password')
api_username = config.get('Re2o', 'username')
global api_client
api_client = Re2oAPIClient(
api_hostname, api_username, api_password, use_tls=True)
class RadiusdHandler(logging.Handler):
"""Handler de logs pour freeradius"""
"""Logs handler for freeradius"""
def emit(self, record):
"""Process un message de log, en convertissant les niveaux"""
"""Log message processing, level are converted"""
if record.levelno >= logging.WARN:
rad_sig = radiusd.L_ERR
elif record.levelno >= logging.INFO:
@ -76,7 +63,7 @@ class RadiusdHandler(logging.Handler):
radiusd.radlog(rad_sig, str(record.msg))
# Initialisation d'un logger (pour logguer unifi )
# Init for logging
logger = logging.getLogger("auth.py")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(name)s: [%(levelname)s] %(message)s")
@ -86,17 +73,15 @@ logger.addHandler(handler)
def radius_event(fun):
"""Décorateur pour les fonctions d'interfaces avec radius.
Une telle fonction prend un uniquement argument, qui est une liste de
tuples (clé, valeur) et renvoie un triplet dont les composantes sont :
* le code de retour (voir radiusd.RLM_MODULE_* )
* un tuple de couples (clé, valeur) pour les valeurs de réponse (accès ok
et autres trucs du genre)
* un tuple de couples (clé, valeur) pour les valeurs internes à mettre à
jour (mot de passe par exemple)
"""Decorator for freeradius fonction with radius.
This function take a unique argument which is a list of tuples (key, value)
and return a tuple of 3 values which are:
* return code (see radiusd.RLM_MODULE_* )
* a tuple of 2 elements for response value (access ok , etc)
* a tuple of 2 elements for internal value to update (password for example)
On se contente avec ce décorateur (pour l'instant) de convertir la liste de
tuples en entrée en un dictionnaire."""
Here, we convert the list of tuples into a dictionnary.
"""
def new_f(auth_data):
""" The function transforming the tuples as dict """
@ -125,19 +110,39 @@ def radius_event(fun):
@radius_event
def instantiate(*_):
"""Usefull for instantiate ldap connexions otherwise,
do nothing"""
"""Instantiate api connection
"""
logger.info("Instantiation")
path = (os.path.dirname(os.path.abspath(__file__)))
config = ConfigParser()
config.read(path+'/config.ini')
api_hostname = config.get('Re2o', 'hostname')
api_password = config.get('Re2o', 'password')
api_username = config.get('Re2o', 'username')
global api_client
api_client = Re2oAPIClient(
api_hostname, api_username, api_password, use_tls=True)
@radius_event
def authorize(data):
# Pour les requetes proxifiees, on split
"""Here, we test if the Nas is known.
- If the nas is unknown, we assume that it is a 802.1X request,
- If the nas is known, we apply the 802.1X if enabled,
- It the nas is known AND nas auth is enabled with mac address, returns accept here
"""
nas = data.get("NAS-IP-Address", data.get("NAS-Identifier", None))
username = data.get("User-Name", "")
# For proxified request, split
username = username.split("@", 1)[0]
mac = data.get("Calling-Station-Id", "")
# Get all required objects from API
data_from_api = api_client.view(
"radius/authorize/{0}/{1}/{2}".format(
urllib.parse.quote(nas or "None", safe=""),
@ -149,7 +154,7 @@ def authorize(data):
user = data_from_api["user"]
user_interface = data_from_api["user_interface"]
if nas_type and nas_type["port_access_mode"] == "802.1X":
if not nas_type or nas_type and nas_type["port_access_mode"] == "802.1X":
result, log, password = check_user_machine_and_register(
nas_type, user, user_interface, nas, username, mac)
logger.info(log.encode("utf-8"))
@ -177,6 +182,7 @@ def post_auth(data):
nas_port = data.get("NAS-Port-Id", data.get("NAS-Port", None))
mac = data.get("Calling-Station-Id", None)
# Get all required objects from API
data_from_api = api_client.view(
"radius/post_auth/{0}/{1}/{2}".format(
urllib.parse.quote(nas or "None", safe=""),
@ -188,12 +194,14 @@ def post_auth(data):
port = data_from_api["port"]
switch = data_from_api["switch"]
# If proxified request
if not nas_type:
logger.info("Proxified request, nas unknown")
return radiusd.RLM_MODULE_OK
# If it is a switch
# If the request is from a switch (wired connection)
if switch:
# For logging
sw_name = switch["name"] or "?"
room = "Unknown port"
if port:
@ -211,7 +219,7 @@ def post_auth(data):
)
logger.info(log_message)
# Wired connexion
# Apply vlan from decide_vlan_switch
return (
radiusd.RLM_MODULE_UPDATED,
(
@ -232,6 +240,7 @@ def post_auth(data):
return (radiusd.RLM_MODULE_REJECT, tuple(attributes), ())
# Else it is from wifi
else:
return radiusd.RLM_MODULE_OK
@ -239,11 +248,16 @@ def post_auth(data):
def check_user_machine_and_register(nas_type, user, user_interface, nas_id, username, mac_address):
"""Check if username and mac are registered. Register it if unknown.
Return the user ntlm password if everything is ok.
Used for 802.1X auth"""
Used for 802.1X auth
"""
if not user:
# No username provided
return (False, "User unknown", "")
if not user["access"]:
return (False, "Invalid connexion (non-contributing user)", "")
if user_interface:
if user_interface["user_pk"] != user["pk"]:
return (
@ -251,9 +265,12 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user
"Mac address registered on another user account",
"",
)
elif not user_interface["active"]:
return (False, "Interface/Machine disabled", "")
elif not user_interface["ipv4"]:
# Try to autoassign ip
try:
api_client.view(
"radius/assign_ip/{0}".format(
@ -264,7 +281,9 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user
return (False, "Error during ip assignement %s" % err.response.text, "")
else:
return (True, "Access ok", user.get("pwd_ntlm", ""))
elif nas_type:
# The interface is not yet registred, try to autoregister if enabled
if nas_type["autocapture_mac"]:
try:
api_client.view(
@ -276,7 +295,7 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user
return (True, "Access Ok, Registering mac...", user["pwd_ntlm"])
except HTTPError as err:
return (False, "Error during mac register %s" % err.response.text, "")
return (False, "L'auto capture est désactivée", "")
return (False, "Autoregistering is disabled", "")
else:
return (False, "Unknown interface/machine", "")
else:
@ -284,6 +303,7 @@ def check_user_machine_and_register(nas_type, user, user_interface, nas_id, user
def set_radius_attributes_values(attributes, values):
"""Set values of parameters in radius attributes"""
return (
(str(attribute["attribute"]), str(attribute["value"] % values))
for attribute in attributes
@ -292,19 +312,21 @@ def set_radius_attributes_values(attributes, values):
def decide_vlan_switch(data_from_api, user_mac, nas_port):
"""Function for selecting vlan for a switch with wired mac auth radius.
Several modes are available :
Two modes exist : in strict mode, a registered user cannot connect with
their machines in a non-registered user room
Sequentially :
- all modes:
- unknown NAS : VLAN_OK,
- unknown port : Decision set in Re2o RadiusOption
- No radius on this port : VLAN_OK
- force : returns vlan provided by the database
- force : replace VLAN_OK with vlan provided by the database
- mode strict:
- no room : Decision set in Re2o RadiusOption,
- no user in this room : Reject,
- user of this room is banned or disable : Reject,
- user of this room non-contributor and not whitelisted:
Decision set in Re2o RadiusOption
- mode common :
- all modes :
- mac-address already registered:
- related user non contributor / interface disabled:
Decision set in Re2o RadiusOption
@ -313,17 +335,16 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port):
- user contributing : VLAN_OK (can assign ipv4 if needed)
- unknown interface :
- register mac disabled : Decision set in Re2o RadiusOption
- register mac enabled : redirect to webauth
- register mac enabled : redirect to webauth (not implemented)
Returns:
tuple with :
- Switch name (str)
- Room (str)
- Reason of the decision (str)
- vlan_id (int)
- decision (bool)
- Other Attributs (attribut:str, operator:str, value:str)
- Other Attributs (attribut:str, value:str)
"""
# Get values from api
nas_type = data_from_api["nas"]
room_users = data_from_api["room_users"]
port = data_from_api["port"]
@ -335,17 +356,17 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port):
RADIUS_OPTION_REJECT = data_from_api["RADIUS_OPTION_REJECT"]
USER_STATE_ACTIVE = data_from_api["USER_STATE_ACTIVE"]
# Values which can be used as parameters in radius attributes
attributes_kwargs = {
"client_mac": str(user_mac),
# magic split
"switch_port": str(nas_port.split(".")[0].split("/")[-1][-2:]),
"switch_ip": str(switch["ipv4"])
}
# Get port from switch and port number
extra_log = ""
# If the port is unknwon, go to default vlan
# We don't have enought information to make a better decision
# If the port is unknown, do as in RadiusOption
if not port or not port_profile:
return (
"Unknown port",
@ -355,7 +376,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port):
radius_option["unknown_port_attributes"], attributes_kwargs),
)
# If a vlan is precised in port config, we use it
# If a vlan is precised in port config, we use it in place of VLAN_OK
if port_profile["vlan_untagged"]:
DECISION_VLAN = int(port_profile["vlan_untagged"]["vlan_id"])
extra_log = "Force sur vlan " + str(DECISION_VLAN)
@ -390,7 +411,7 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port):
# Otherwise, we are in mac radius.
# If strict mode is enabled, we check every user related with this port. If
# one user or more is not enabled, we reject to prevent from sharing or
# all users and clubs are disabled, we reject to prevent from sharing or
# spoofing mac.
if port_profile["radius_mode"] == "STRICT":
if not port["room"]:
@ -438,12 +459,11 @@ def decide_vlan_switch(data_from_api, user_mac, nas_port):
)
# else: user OK, so we check MAC now
# If we are authenticating with mac, we look for the interfaces and its mac address
if port_profile["radius_mode"] == "COMMON" or port_profile["radius_mode"] == "STRICT":
# If mac is unknown,
if not user_interface:
# We try to register mac, if autocapture is enabled
# Final decision depend on RADIUSOption set in re2o
# Something is not implemented here...
if nas_type["autocapture_mac"]:
return (
"Unknown mac/interface",

View file

@ -105,12 +105,18 @@ class RadiusOptionSerializer(Serializer):
class AuthorizeResponseSerializer(Serializer):
"""Serializer for AuthorizeResponse objects
See views.py for the declaration of AuthorizeResponse
"""
nas = NasSerializer(read_only=True)
user = UserSerializer(read_only=True)
user_interface = InterfaceSerializer(read_only=True)
class PostAuthResponseSerializer(Serializer):
"""Serializer for PostAuthResponse objects
See views.py for the declaration of PostAuthResponse
"""
nas = NasSerializer(read_only=True)
room_users = UserSerializer(many=True)
port = PortSerializer()

View file

@ -28,6 +28,5 @@ urls_functional_view = [
views.post_auth, None),
(r"radius/autoregister/(?P<nas_id>[^/]+)/(?P<username>.+)/(?P<mac_address>[0-9a-fA-F\:\-]{17})$",
views.autoregister_machine, None),
(r"radius/assign_ip/(?P<mac_address>[0-9a-fA-F\:\-]{17})$",
views.assign_ip, None),
(r"radius/assign_ip/(?P<mac_address>[0-9a-fA-F\:\-]{17})$", views.assign_ip, None),
]

View file

@ -33,18 +33,23 @@ from topologie.models import Port, Switch
class AuthorizeResponse:
"""Contains objects the radius needs for the Authorize step
"""
def __init__(self, nas, user, user_interface):
self.nas = nas
self.user = user
self.user_interface = user_interface
def can_view(self, user):
"""Temp method to bypass ACL
"""
return [True]
@api_view(['GET'])
def authorize(request, nas_id, username, mac_address):
"""Return objects the radius need for the Authorize step
"""Return objects the radius needs for the Authorize step
Parameters:
nas_id (string): NAS name or ipv4
@ -52,9 +57,10 @@ def authorize(request, nas_id, username, mac_address):
mac_address (string): mac address of the device which is trying to connect
Return:
AuthorizeResponse: contains all the informations
AuthorizeResponse: contains all required informations
"""
# get the Nas object which made the request (if exists)
nas_interface = Interface.objects.filter(
Q(domain=Domain.objects.filter(name=nas_id))
| Q(ipv4=IpList.objects.filter(ipv4=nas_id))
@ -64,7 +70,11 @@ def authorize(request, nas_id, username, mac_address):
nas_type = Nas.objects.filter(
nas_type=nas_interface.machine_type).first()
# get the User corresponding to the username in the URL
# If no username was provided (wired connection), username="None"
user = User.objects.filter(pseudo__iexact=username).first()
# get the interface which is trying to connect (if already created)
user_interface = Interface.objects.filter(mac_address=mac_address).first()
serialized = serializers.AuthorizeResponseSerializer(
@ -74,6 +84,9 @@ def authorize(request, nas_id, username, mac_address):
class PostAuthResponse:
"""Contains objects the radius needs for the Post-Auth step
"""
def __init__(self, nas, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE):
self.nas = nas
self.room_users = room_users
@ -92,7 +105,18 @@ class PostAuthResponse:
@api_view(['GET'])
def post_auth(request, nas_id, nas_port, user_mac):
# get nas_type
"""Return objects the radius needs for the Post-Auth step
Parameters:
nas_id (string): NAS name or ipv4
nas_port (string): NAS port from wich the request came. Work with Cisco, HP and Juniper convention
user_mac (string): mac address of the device which is trying to connect
Return:
PostAuthResponse: contains all required informations
"""
# get the Nas object which made the request (if exists)
nas_interface = Interface.objects.prefetch_related("machine__switch__stack").filter(
Q(domain=Domain.objects.filter(name=nas_id))
| Q(ipv4=IpList.objects.filter(ipv4=nas_id))
@ -102,14 +126,17 @@ def post_auth(request, nas_id, nas_port, user_mac):
nas_type = Nas.objects.filter(
nas_type=nas_interface.machine_type).first()
# get switch
# get the switch (if wired connection)
switch = None
if nas_interface:
switch = Switch.objects.filter(
machine_ptr=nas_interface.machine).first()
# If the switch is part of a stack, get the correct object
if hasattr(nas_interface.machine, "switch"):
stack = nas_interface.machine.switch.stack
if stack:
# magic split
id_stack_member = nas_port.split("-")[1].split("/")[0]
switch = (
Switch.objects.filter(stack=stack)
@ -117,9 +144,10 @@ def post_auth(request, nas_id, nas_port, user_mac):
.first()
)
# get port
# get the switch port
port = None
if nas_port and nas_port != "None":
# magic split
port_number = nas_port.split(".")[0].split("/")[-1][-2:]
port = Port.objects.filter(switch=switch, port=port_number).first()
@ -127,7 +155,7 @@ def post_auth(request, nas_id, nas_port, user_mac):
if port:
port_profile = port.get_port_profile
# get user_interface
# get the interface which is trying to connect (if already created)
user_interface = (
Interface.objects.filter(mac_address=user_mac)
.select_related("machine__user")
@ -135,20 +163,22 @@ def post_auth(request, nas_id, nas_port, user_mac):
.first()
)
# get room users
# get all users and clubs of the room
room_users = []
if port:
room_users = User.objects.filter(
Q(club__room=port.room) | Q(adherent__room=port.room)
)
# get radius options
# get all radius options
radius_option = RadiusOption.objects.first()
print(radius_option)
# get a few class constants the radius will need
EMAIL_STATE_UNVERIFIED = User.EMAIL_STATE_UNVERIFIED
RADIUS_OPTION_REJECT = RadiusOption.REJECT
USER_STATE_ACTIVE = User.STATE_ACTIVE
serialized = serializers.PostAuthResponseSerializer(
PostAuthResponse(nas_type, room_users, port, port_profile, switch, user_interface, radius_option, EMAIL_STATE_UNVERIFIED, RADIUS_OPTION_REJECT, USER_STATE_ACTIVE))
@ -157,6 +187,17 @@ def post_auth(request, nas_id, nas_port, user_mac):
@api_view(['GET'])
def autoregister_machine(request, nas_id, username, mac_address):
"""Autoregister machine in the Authorize step of the radius
Parameters:
nas_id (string): NAS name or ipv4
username (string): username of the user who is trying to connect
mac_address (string): mac address of the device which is trying to connect
Return:
200 if autoregistering worked
400 if it failed, and the reason why
"""
nas_interface = Interface.objects.filter(
Q(domain=Domain.objects.filter(name=nas_id))
| Q(ipv4=IpList.objects.filter(ipv4=nas_id))
@ -170,12 +211,21 @@ def autoregister_machine(request, nas_id, username, mac_address):
result, reason = user.autoregister_machine(mac_address, nas_type)
if result:
return Response(data=reason)
return Response(reason)
return Response(reason, status=400)
@api_view(['GET'])
def assign_ip(request, mac_address):
"""Autoassign ip in the Authorize and Post-Auth steps of the Radius
Parameters:
mac_address (string): mac address of the device which is trying to connect
Return:
200 if it worked
400 if it failed, and the reason why
"""
interface = (
Interface.objects.filter(mac_address=mac_address)
.first()