diff --git a/apt_requirements_radius.txt b/apt_requirements_radius.txt new file mode 100644 index 00000000..2d4e9bde --- /dev/null +++ b/apt_requirements_radius.txt @@ -0,0 +1,23 @@ +python-django +python-dateutil +texlive-latex-base +texlive-fonts-recommended +python-djangorestframework +python-django-reversion +python-pip +libsasl2-dev libldap2-dev +libssl-dev +python-crypto +python-git +javascript-common +libjs-jquery +libjs-jquery-ui +libjs-jquery-timepicker +libjs-bootstrap +fonts-font-awesome +graphviz +git +gettext +freeradius-common +freeradius-python2 +python-mysqldb diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 7245013e..c2cb3fed 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -117,8 +117,11 @@ def radius_event(fun): # (str,str) : rlm_python ne digère PAS les unicodes return fun(data) except Exception as err: + exc_type, exc_instance, exc_traceback = sys.exc_info() + formatted_traceback = ''.join(traceback.format_tb( + exc_traceback)) logger.error('Failed %r on data %r' % (err, auth_data)) - logger.debug('Function %r, Traceback: %s' % (fun, repr(traceback.format_stack()))) + logger.error('Function %r, Traceback : %r' % (fun, formatted_traceback)) return radiusd.RLM_MODULE_FAIL return new_f @@ -225,7 +228,7 @@ def post_auth(data): # La ligne suivante fonctionne pour cisco, HP et Juniper port = port.split(".")[0].split('/')[-1][-2:] out = decide_vlan_switch(nas_machine, nas_type, port, mac) - sw_name, room, reason, vlan_id, decision = out + sw_name, room, reason, vlan_id, decision, attributes = out if decision: log_message = '(fil) %s -> %s [%s%s]' % ( @@ -243,7 +246,7 @@ def post_auth(data): ("Tunnel-Type", "VLAN"), ("Tunnel-Medium-Type", "IEEE-802"), ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)), - ), + ) + tuple(attributes), () ) else: @@ -254,7 +257,11 @@ def post_auth(data): ) logger.info(log_message) - return radiusd.RLM_MODULE_REJECT + return ( + radiusd.RLM_MODULE_REJECT, + tuple(attributes), + () + ) else: return radiusd.RLM_MODULE_OK @@ -363,18 +370,32 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, - raison de la décision (str) - vlan_id (int) - decision (bool) + - Attributs supplémentaires (attribut:str, operateur:str, valeur:str) """ + attributes_kwargs = { + 'client_mac' : str(mac_address), + 'switch_port' : str(port_number), + } # Get port from switch and port number extra_log = "" # Si le NAS est inconnu, on place sur le vlan defaut if not nas_machine: - return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True) + return ( + '?', + u'Chambre inconnue', + u'Nas inconnu', + RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, + True, + RadiusOption.get_attributes('ok_attributes', attributes_kwargs) + ) sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine))) + switch = Switch.objects.filter(machine_ptr=nas_machine).first() + attributes_kwargs['switch_ip'] = str(switch.ipv4) port = (Port.objects .filter( - switch=Switch.objects.filter(machine_ptr=nas_machine), + switch=switch, port=port_number ) .first()) @@ -385,10 +406,11 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, if not port: return ( sw_name, - "Chambre inconnue", + "Port inconnu", u'Port inconnu', getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None), - RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT + RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT, + RadiusOption.get_attributes('unknown_port_attributes', attributes_kwargs) ) # On récupère le profil du port @@ -399,12 +421,14 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, if port_profile.vlan_untagged: DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) extra_log = u"Force sur vlan " + str(DECISION_VLAN) + attributes = () else: DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok').vlan_id + attributes = RadiusOption.get_attributes('ok_attributes', attributes_kwargs) # Si le port est désactivé, on rejette la connexion if not port.state: - return (sw_name, port.room, u'Port desactive', None, False) + return (sw_name, port.room, u'Port desactive', None, False, ()) # Si radius est désactivé, on laisse passer if port_profile.radius_type == 'NO': @@ -412,7 +436,9 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, "", u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN, - True) + True, + attributes + ) # Si le 802.1X est activé sur ce port, cela veut dire que la personne a # été accept précédemment @@ -424,7 +450,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, room, u'Acceptation authentification 802.1X', DECISION_VLAN, - True + True, + attributes ) # Sinon, cela veut dire qu'on fait de l'auth radius par mac @@ -441,7 +468,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, "Inconnue", u'Chambre inconnue', getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None), - RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT + RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT, + RadiusOption.get_attributes('unknown_room_attributes', attributes_kwargs), ) room_user = User.objects.filter( @@ -451,18 +479,20 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, return ( sw_name, room, - u'Chambre non cotisante -> Web redirect', - None, - False + u'Chambre non cotisante', + getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT, + RadiusOption.get_attributes('non_member_attributes', attributes_kwargs), ) for user in room_user: if user.is_ban() or user.state != User.STATE_ACTIVE: return ( sw_name, room, - u'Utilisateur banni ou desactive -> Web redirect', - None, - False + u'Utilisateur banni ou desactive', + getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT, + RadiusOption.get_attributes('banned_attributes', attributes_kwargs), ) elif not (user.is_connected() or user.is_whitelisted()): return ( @@ -470,7 +500,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, room, u'Utilisateur non cotisant', getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None), - RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT + RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT, + RadiusOption.get_attributes('non_member_attributes', attributes_kwargs), ) # else: user OK, on passe à la verif MAC @@ -491,9 +522,10 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, return ( sw_name, room, - u'Machine Inconnue -> Web redirect', - None, - False + u'Machine Inconnue', + getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT, + RadiusOption.get_attributes('unknown_machine_attributes', attributes_kwargs), ) # Sinon on bascule sur la politique définie dans les options # radius. @@ -503,7 +535,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, "", u'Machine inconnue', getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None), - RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT + RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT, + RadiusOption.get_attributes('unknown_machine_attributes', attributes_kwargs), ) # L'interface a été trouvée, on vérifie qu'elle est active, @@ -518,7 +551,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, room, u'Adherent banni', getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None), - RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT + RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT, + RadiusOption.get_attributes('banned_attributes', attributes_kwargs), ) if not interface.is_active: return ( @@ -526,7 +560,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, room, u'Machine non active / adherent non cotisant', getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None), - RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT + RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT, + RadiusOption.get_attributes('non_member_attributes', attributes_kwargs), ) # Si on choisi de placer les machines sur le vlan # correspondant à leur type : @@ -539,7 +574,8 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, room, u"Ok, Reassignation de l'ipv4" + extra_log, DECISION_VLAN, - True + True, + attributes ) else: return ( @@ -547,5 +583,6 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, room, u'Machine OK' + extra_log, DECISION_VLAN, - True + True, + attributes ) diff --git a/freeradius_utils/freeradius3/clients.conf b/freeradius_utils/freeradius3/clients.conf new file mode 100644 index 00000000..186181d4 --- /dev/null +++ b/freeradius_utils/freeradius3/clients.conf @@ -0,0 +1,29 @@ +# Your client radius configuration below +client radius-filaire { + ipaddr = + netmask = + secret = + require_message_authenticator = no + nastype = other + virtual_server = radius-filaire +} +client radius-wifi { + ipaddr = + netmask = + secret = + require_message_authenticator = no + nastype = other + virtual_server = radius-wifi +} + +# Parangon (federez) +client parangon { + ipaddr = 185.230.78.47 + secret = please_ask_for_a_secret_to_federez_admin +} + +# Dodecagon (federez) +client dodecagon { + ipaddr = 163.172.48.168 + secret = please_ask_for_a_secret_to_federez_admin +} diff --git a/install_re2o.sh b/install_re2o.sh index b8a8c87d..6cc9a2fb 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -4,11 +4,21 @@ SETTINGS_LOCAL_FILE='re2o/settings_local.py' SETTINGS_EXAMPLE_FILE='re2o/settings_local.example.py' APT_REQ_FILE="apt_requirements.txt" +APT_RADIUS_REQ_FILE="apt_requirements_radius.txt" PIP_REQ_FILE="pip_requirements.txt" +PIP_RADIUS_REQ_FILE="pip_requirements_radius.txt" LDIF_DB_FILE="install_utils/db.ldiff" LDIF_SCHEMA_FILE="install_utils/schema.ldiff" +FREERADIUS_CLIENTS="freeradius_utils/freeradius3/clients.conf" +FREERADIUS_AUTH="freeradius_utils/auth.py" +FREERADIUS_RADIUSD="freeradius_utils/freeradius3/radiusd.conf" +FREERADIUS_MOD_PYTHON="freeradius_utils/freeradius3/mods-enabled/python" +FREERADIUS_MOD_EAP="freeradius_utils/freeradius3/mods-enabled/eap" +FREERADIUS_SITE_DEFAULT="freeradius_utils/freeradius3/sites-enabled/default" +FREERADIUS_SITE_INNER_TUNNEL="freeradius_utils/freeradius3/sites-enabled/inner-tunnel" +EDITOR="nano" VALUE= # global value used to return values by some functions @@ -73,6 +83,44 @@ install_requirements() { } +install_radius_requirements() { + ### Usage: install_radius_requirements + # + # This function will install the required packages from APT repository + # and Pypi repository. Those packages are all required for Re2o to work + # properly. + ### + + echo "Setting up the required packages ..." + cat $APT_RADIUS_REQ_FILE | xargs apt-get -y install + python -m pip install -r $PIP_RADIUS_REQ_FILE + echo "Setting up the required packages: Done" +} + + +configure_radius() { + ### Usage: configure_radius + # + # This function configures freeradius. + ### + echo "Configuring Freeradius ..." + + cat $FREERADIUS_CLIENTS >> /etc/freeradius/3.0/clients.conf + ln -fs $(pwd)/$FREERADIUS_AUTH /etc/freeradius/3.0/auth.py + ln -fs $(pwd)/$FREERADIUS_RADIUSD /etc/freeradius/3.0/radiusd.conf + ln -fs $(pwd)/$FREERADIUS_MOD_PYTHON /etc/freeradius/3.0/mods-enabled/python + ln -fs $(pwd)/$FREERADIUS_MOD_EAP /etc/freeradius/3.0/mods-enabled/eap + ln -fs $(pwd)/$FREERADIUS_SITE_DEFAULT /etc/freeradius/3.0/sites-enabled/default + ln -fs $(pwd)/$FREERADIUS_SITE_INNER_TUNNEL /etc/freeradius/3.0/sites-enabled/inner-tunnel + _ask_value "Ready to edit clients.conf ?" "yes" + $EDITOR /etc/freeradius/3.0/clients.conf + + + echo "Configuring Freeradius: Done" + +} + + install_database() { ### Usage: install_database @@ -715,6 +763,265 @@ interactive_guide() { +interactive_radius_guide() { + ### Usage: interactive_radius_guide + # + # This function will guide through the automated setup of radius with + # Re2o by asking the user for some informations and some installation + # choices. It will then proceed to setup and configuration of the + # required tools according to the user choices. + ### + + echo "Re2o setup !" + echo "This tool will help you setup re2o radius. It is highly recommended to use a Debian clean server for this operation." + + echo "Installing basic packages required for this script to work ..." + apt-get -y install sudo dialog + echo "Installing basic packages required for this script to work: Done" + + # Common setup for the dialog prompts + export DEBIAN_FRONTEND=noninteractive + HEIGHT=20 + WIDTH=60 + CHOICE_HEIGHT=4 + + + + ############# + ## Welcome ## + ############# + + BACKTITLE="Re2o setup" + + # Welcome prompt + TITLE="Welcome" + MSGBOX="This tool will help you setup re2o. It is highly recommended to use a Debian clean server for this operation." + init="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --msgbox "$MSGBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + + + ###################### + ## Database options ## + ###################### + + BACKTITLE="Re2o setup - configuration of the database" + + # Prompt for choosing the database engine + TITLE="Database engine" + MENU="Which engine should be used as the database ?" + OPTIONS=(1 "mysql" + 2 "postgresql") + sql_bdd_type="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --menu "$MENU" \ + $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" + + # Prompt for choosing the database location + TITLE="SQL location" + MENU="Where to install the SQL database ? + * 'Local' will setup everything automatically but is not recommended for production + * 'Remote' will ask you to manually perform some setup commands on the remote server" + OPTIONS=(1 "Local" + 2 "Remote") + sql_is_local="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --menu "$MENU" \ + $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" + + if [ $sql_is_local == 2 ]; then + # Prompt to enter the remote database hostname + TITLE="SQL hostname" + INPUTBOX="The hostname of the remote SQL database" + sql_host="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + # Prompt to enter the remote database name + TITLE="SQL database name" + INPUTBOX="The name of the remote SQL database" + sql_name="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + # Prompt to enter the remote database username + TITLE="SQL username" + INPUTBOX="The username to access the remote SQL database" + sql_login="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + clear + else + # Use of default values for local setup + sql_name="re2o" + sql_login="re2o" + sql_host="localhost" + fi + + # Prompt to enter the database password + TITLE="SQL password" + INPUTBOX="The password to access the SQL database" + sql_password="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + + + ################## + ## LDAP options ## + ################## + + BACKTITLE="Re2o setup - configuration of the LDAP" + + # Prompt to choose the LDAP location + TITLE="LDAP location" + MENU="Where would you like to install the LDAP ? + * 'Local' will setup everything automatically but is not recommended for production + * 'Remote' will ask you to manually perform some setup commands on the remote server" + OPTIONS=(1 "Local" + 2 "Remote") + ldap_is_local="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --menu "$MENU" \ + $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" + + # Prompt to enter the LDAP domain extension + TITLE="Domain extension" + INPUTBOX="The local domain extension to use (e.g. 'example.net'). This is used in the LDAP configuration." + extension_locale="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + # Building the DN of the LDAP from the extension + IFS='.' read -a extension_locale_array <<< $extension_locale + for i in "${extension_locale_array[@]}" + do + ldap_dn+="dc=$i," + done + ldap_dn="${ldap_dn::-1}" + + if [ "$ldap_is_local" == 2 ]; then + # Prompt to enter the remote LDAP hostname + TITLE="LDAP hostname" + INPUTBOX="The hostname of the remote LDAP" + ldap_host="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + # Prompt to choose if TLS should be activated or not for the LDAP + TITLE="TLS on LDAP" + MENU="Would you like to activate TLS for communicating with the remote LDAP ?" + OPTIONS=(1 "Yes" + 2 "No") + ldap_tls="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --MENU "$MENU" \ + $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" + + # Prompt to enter the admin's CN of the remote LDAP + TITLE="CN of amdin user" + INPUTBOX="The CN entry for the admin user of the remote LDAP" + ldap_cn="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + else + ldap_cn="cn=admin," + ldap_cn+="$ldap_dn" + ldap_host="localhost" + ldap_tls=2 + fi + + # Prompt to enter the LDAP password + TITLE="LDAP password" + INPUTBOX="The password to access the LDAP" + ldap_password="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$INPUTBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + + + ######################### + ## Mail server options ## + ######################### + + BACKTITLE="Re2o setup - configuration of the mail server" + + # Prompt to enter the hostname of the mail server + TITLE="Mail server hostname" + INPUTBOX="The hostname of the mail server to use" + email_host="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --inputbox "$TITLE" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + # Prompt to choose the port of the mail server + TITLE="Mail server port" + MENU="Which port (thus which protocol) to use to contact the mail server" + OPTIONS=(25 "SMTP" + 465 "SMTPS" + 587 "Submission") + email_port="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --menu "$MENU" \ + $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" + + + + ############################### + ## End of configuration step ## + ############################### + + BACKTITLE="Re2o setup" + + # Prompt to inform the config setup is over + TITLE="End of configuration step" + MSGBOX="The configuration step is now finished. The script will now perform the following actions: + * Install the required packages + * Install and setup the requested database if 'local' has been selected + * Install and setup the ldap if 'local' has been selected + * Write a local version of 'settings_local.py' file with the previously given informations + * Apply the Django migrations for the project + * Install and setup freeradius" + end_config="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --msgbox "$MSGBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" + + clear + + + + ################################ + ## Perform the actual actions ## + ################################ + + install_radius_requirements + + configure_radius + + install_database "$sql_bdd_type" "$sql_is_local" "$sql_name" "$sql_login" "$sql_password" + + install_ldap "$ldap_is_local" "$ldap_password" "$ldap_dn" + + + write_settings_file "$sql_bdd_type" "$sql_host" "$sql_name" "$sql_login" "$sql_password" \ + "$ldap_cn" "$ldap_tls" "$ldap_password" "$ldap_host" "$ldap_dn" \ + "$email_host" "$email_port" "$extension_locale" "$url_server" + + update_django + + + ########################### + ## End of the setup step ## + ########################### + + BACKTITLE="Re2o setup" + + # Prompt to inform the installation process is over + TITLE="End of the setup" + MSGBOX="You can now use your RADIUS server." + end="$(dialog --clear --backtitle "$BACKTITLE" \ + --title "$TITLE" --msgbox "$MSGBOX" \ + $HEIGHT $WIDTH 2>&1 >/dev/tty)" +} + + + + interactive_update_settings() { ### Usage: interactvie_update_settings @@ -763,9 +1070,13 @@ main_function() { echo " * {help} ---------- Display this quick usage documentation" echo " * {setup} --------- Launch the full interactive guide to setup entirely" echo " re2o from scratch" + echo " * {setup-radius} -- Launch the full interactive guide to setup entirely" + echo " re2o radius from scratch" echo " * {update} -------- Collect frontend statics, install the missing APT" echo " and pip packages, copy LaTeX templates files" echo " and apply the migrations to the DB" + echo " * {update-radius} - Update radius apt and pip packages and copy radius" + echo " configuration files to their proper location." echo " * {update-django} - Apply Django migration and collect frontend statics" echo " * {copy-template-files} - Copy LaTeX templates files to media/templates" echo " * {update-packages} Install the missing APT and pip packages" @@ -797,12 +1108,21 @@ main_function() { interactive_guide ;; + setup-radius ) + interactive_radius_guide + ;; + update ) install_requirements copy_templates_files update_django ;; + update-radius ) + install_radius_requirements + configure_radius + ;; + copy-templates-files ) copy_templates_files ;; diff --git a/pip_requirements_radius.txt b/pip_requirements_radius.txt new file mode 100644 index 00000000..c49222aa --- /dev/null +++ b/pip_requirements_radius.txt @@ -0,0 +1,3 @@ +django-bootstrap3 +django-macaddress +django-ldapdb==1.3.0 diff --git a/preferences/forms.py b/preferences/forms.py index d2bede7c..e216ea1f 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -44,7 +44,8 @@ from .models import ( SwitchManagementCred, RadiusOption, CotisationsOption, - DocumentTemplate + DocumentTemplate, + RadiusAttribute ) from topologie.models import Switch @@ -411,3 +412,33 @@ class DelDocumentTemplateForm(FormRevMixin, Form): self.fields['document_templates'].queryset = instances else: self.fields['document_templates'].queryset = Banque.objects.all() + + +class RadiusAttributeForm(ModelForm): + """Edit and add RADIUS attributes.""" + class Meta: + model = RadiusAttribute + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(RadiusAttributeForm, self).__init__(*args, prefix=prefix, **kwargs) + + +class DelRadiusAttributeForm(Form): + """Delete RADIUS attributes""" + attributes = forms.ModelMultipleChoiceField( + queryset=RadiusAttribute.objects.none(), + label=_("Current attributes"), + widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelServiceForm, self).__init__(*args, **kwargs) + if instances: + self.fields['attributes'].queryset = instances + else: + self.fields['attributes'].queryset = Attributes.objects.all() + + diff --git a/preferences/migrations/0062_auto_20190910_1909.py b/preferences/migrations/0062_auto_20190910_1909.py new file mode 100644 index 00000000..d121ba7c --- /dev/null +++ b/preferences/migrations/0062_auto_20190910_1909.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-09-10 17:09 +from __future__ import unicode_literals + +from django.db import migrations, models +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0061_optionaluser_allow_archived_connexion'), + ] + + operations = [ + migrations.CreateModel( + name='RadiusAttribute', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attribute', models.CharField(help_text='See http://freeradius.org/rfc/attributes.html', max_length=255, verbose_name='Attribute')), + ('value', models.CharField(max_length=255, verbose_name='Value')), + ('comment', models.TextField(blank=True, default='', help_text='Use this field to document this attribute.', verbose_name='Comment')), + ], + options={ + 'verbose_name': 'RADIUS attribute', + 'verbose_name_plural': 'RADIUS attributes', + }, + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), + ), + migrations.AddField( + model_name='radiusoption', + name='banned_attributes', + field=models.ManyToManyField(blank=True, help_text='Answer attributes for banned users.', related_name='banned_attribute', to='preferences.RadiusAttribute', verbose_name='Banned attributes.'), + ), + migrations.AddField( + model_name='radiusoption', + name='non_member_attributes', + field=models.ManyToManyField(blank=True, help_text='Answer attributes for non members.', related_name='non_member_attribute', to='preferences.RadiusAttribute', verbose_name='Non member attributes.'), + ), + migrations.AddField( + model_name='radiusoption', + name='ok_attributes', + field=models.ManyToManyField(blank=True, help_text='Answer attributes for accepted users.', related_name='ok_attribute', to='preferences.RadiusAttribute', verbose_name='Accepted users attributes.'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_machine_attributes', + field=models.ManyToManyField(blank=True, help_text='Answer attributes for unknown machines.', related_name='unknown_machine_attribute', to='preferences.RadiusAttribute', verbose_name='Unknown machines attributes.'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_port_attributes', + field=models.ManyToManyField(blank=True, help_text='Answer attributes for unknown ports.', related_name='unknown_port_attribute', to='preferences.RadiusAttribute', verbose_name='Unknown ports attributes.'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_room_attributes', + field=models.ManyToManyField(blank=True, help_text='Answer attributes for unknown rooms.', related_name='unknown_room_attribute', to='preferences.RadiusAttribute', verbose_name='Unknown rooms attributes.'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 98374e77..f5309b01 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -591,6 +591,32 @@ class MailMessageOption(AclMixin, models.Model): verbose_name = _("email message options") +class RadiusAttribute(RevMixin, AclMixin, models.Model): + class Meta: + verbose_name = _("RADIUS attribute") + verbose_name_plural = _("RADIUS attributes") + + attribute = models.CharField( + max_length=255, + verbose_name=_("Attribute"), + help_text=_("See http://freeradius.org/rfc/attributes.html"), + ) + value = models.CharField( + max_length=255, + verbose_name=_("Value") + ) + comment = models.TextField( + verbose_name=_("Comment"), + help_text=_("Use this field to document this attribute."), + blank=True, + default="" + ) + + def __str__(self): + return ' '.join([self.attribute, self.operator, self.value]) + + + class RadiusOption(AclMixin, PreferencesModel): class Meta: verbose_name = _("RADIUS policy") @@ -628,6 +654,13 @@ class RadiusOption(AclMixin, PreferencesModel): verbose_name=_("Unknown machines VLAN"), help_text=_("VLAN for unknown machines if not rejected") ) + unknown_machine_attributes = models.ManyToManyField( + RadiusAttribute, + related_name='unknown_machine_attribute', + blank=True, + verbose_name=_("Unknown machines attributes."), + help_text=_("Answer attributes for unknown machines."), + ) unknown_port = models.CharField( max_length=32, choices=CHOICE_POLICY, @@ -643,6 +676,13 @@ class RadiusOption(AclMixin, PreferencesModel): verbose_name=_("Unknown ports VLAN"), help_text=_("VLAN for unknown ports if not rejected") ) + unknown_port_attributes = models.ManyToManyField( + RadiusAttribute, + related_name='unknown_port_attribute', + blank=True, + verbose_name=_("Unknown ports attributes."), + help_text=_("Answer attributes for unknown ports."), + ) unknown_room = models.CharField( max_length=32, choices=CHOICE_POLICY, @@ -659,6 +699,13 @@ class RadiusOption(AclMixin, PreferencesModel): verbose_name=_("Unknown rooms VLAN"), help_text=_("VLAN for unknown rooms if not rejected") ) + unknown_room_attributes = models.ManyToManyField( + RadiusAttribute, + related_name='unknown_room_attribute', + blank=True, + verbose_name=_("Unknown rooms attributes."), + help_text=_("Answer attributes for unknown rooms."), + ) non_member = models.CharField( max_length=32, choices=CHOICE_POLICY, @@ -674,6 +721,13 @@ class RadiusOption(AclMixin, PreferencesModel): verbose_name=_("Non members VLAN"), help_text=_("VLAN for non members if not rejected") ) + non_member_attributes = models.ManyToManyField( + RadiusAttribute, + related_name='non_member_attribute', + blank=True, + verbose_name=_("Non member attributes."), + help_text=_("Answer attributes for non members."), + ) banned = models.CharField( max_length=32, choices=CHOICE_POLICY, @@ -689,6 +743,13 @@ class RadiusOption(AclMixin, PreferencesModel): verbose_name=_("Banned users VLAN"), help_text=_("VLAN for banned users if not rejected") ) + banned_attributes = models.ManyToManyField( + RadiusAttribute, + related_name='banned_attribute', + blank=True, + verbose_name=_("Banned attributes."), + help_text=_("Answer attributes for banned users."), + ) vlan_decision_ok = models.OneToOneField( 'machines.Vlan', on_delete=models.PROTECT, @@ -696,6 +757,23 @@ class RadiusOption(AclMixin, PreferencesModel): blank=True, null=True ) + ok_attributes = models.ManyToManyField( + RadiusAttribute, + related_name='ok_attribute', + blank=True, + verbose_name=_("Accepted users attributes."), + help_text=_("Answer attributes for accepted users."), + ) + + @classmethod + def get_attributes(cls, name, attribute_kwargs={}): + return ( + ( + str(attribute.attribute), + str(attribute.value % attribute_kwargs) + ) + for attribute in cls.get_cached_value(name).all() + ) def default_invoice(): diff --git a/preferences/templates/preferences/aff_radiusattributes.html b/preferences/templates/preferences/aff_radiusattributes.html new file mode 100644 index 00000000..8edd8b93 --- /dev/null +++ b/preferences/templates/preferences/aff_radiusattributes.html @@ -0,0 +1,50 @@ +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2018 Hugo Levy-Falk + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} +{% load i18n %} +{% load acl %} +{% load logs_extra %} + + + + + + + + + + {% for attribute in radius_attributes %} + + + + + + {% endfor %} +
{% trans "Attribute" %}{% trans "Comment" %}
{{ attribute }}{{ attribute.comment }} + {% can_edit attribute%} + {% include 'buttons/edit.html' with href='preferences:edit-radiusattribute' id=attribute.id %} + {% acl_end %} + {% can_delete attribute %} + {% include 'buttons/suppr.html' with href='preferences:del-radiusattribute' id=attribute.id %} + {% acl_end %} + {% history_button attribute %} +
+ diff --git a/preferences/templates/preferences/aff_radiusoptions.html b/preferences/templates/preferences/aff_radiusoptions.html index 41cb1846..0e84b44c 100644 --- a/preferences/templates/preferences/aff_radiusoptions.html +++ b/preferences/templates/preferences/aff_radiusoptions.html @@ -32,6 +32,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "VLAN for machines accepted by RADIUS" %} {% blocktrans with vlan_decision_ok=radiusoptions.vlan_decision_ok %}VLAN {{ vlan_decision_ok }}{% endblocktrans %} + {% trans "Attributes" %} +
    + {% for attribute in radiusoptions.ok_attributes.all %} +
  • {{attribute}}
  • + {% endfor %} +
+
@@ -40,6 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Situation" %} {% trans "Behaviour" %} + {% trans "Attributes" %} @@ -51,6 +59,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans with unknown_machine_vlan=radiusoptions.unknown_machine_vlan %}VLAN {{ unknown_machine_vlan }}{% endblocktrans %} {% endif %} + +
    + {% for attribute in radiusoptions.unknown_machine_attributes.all %} +
  • {{attribute}}
  • + {% endfor %} +
+ {% trans "Unknown port" %} @@ -61,6 +76,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans with unknown_port_vlan=radiusoptions.unknown_port_vlan %}VLAN {{ unknown_port_vlan }}{% endblocktrans %} {% endif %} + +
    + {% for attribute in radiusoptions.unknown_port_attributes.all %} +
  • {{attribute}}
  • + {% endfor %} +
+ {% trans "Unknown room" %} @@ -71,6 +93,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans with unknown_room_vlan=radiusoptions.unknown_room_vlan %}VLAN {{ unknown_room_vlan }}{% endblocktrans %} {% endif %} + +
    + {% for attribute in radiusoptions.unknown_room_attributes.all %} +
  • {{attribute}}
  • + {% endfor %} +
+ {% trans "Non member" %} @@ -81,16 +110,30 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans with non_member_vlan=radiusoptions.non_member_vlan %}VLAN {{ non_member_vlan }}{% endblocktrans %} {% endif %} + +
    + {% for attribute in radiusoptions.non_member_attributes.all %} +
  • {{attribute}}
  • + {% endfor %} +
+ {% trans "Banned user" %} - {% if radiusoptions.unknown_port == 'REJECT' %} + {% if radiusoptions.banned == 'REJECT' %} {% trans "Reject" %} {% else %} {% blocktrans with banned_vlan=radiusoptions.banned_vlan %}VLAN {{ banned_vlan }}{% endblocktrans %} {% endif %} + +
    + {% for attribute in radiusoptions.banned_attributes.all %} +
  • {{attribute}}
  • + {% endfor %} +
+ diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 626cb1a3..99ee9be3 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -298,11 +298,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% trans "RADIUS preferences" %}

+
{% trans "RADIUS policies" %}
{% trans "Edit" %} {% include 'preferences/aff_radiusoptions.html' %} +
{% trans "Available RADIUS attributes"%}
+ {% trans " Add an attribute" %} + {% include 'preferences/aff_radiusattributes.html' %}
diff --git a/preferences/urls.py b/preferences/urls.py index 9bfd67d3..d07aa0be 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -126,5 +126,12 @@ urlpatterns = [ views.del_document_template, name='del-document-template' ), + url(r'^add_radiusattribute/$', views.add_radiusattribute, name='add-radiusattribute'), + url( + r'^edit_radiusattribute/(?P[0-9]+)$', + views.edit_radiusattribute, + name='edit-radiusattribute' + ), + url(r'^del_radiusattribute/(?P[0-9]+)$', views.del_radiusattribute, name='del-radiusattribute'), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 90afa2e8..5ccd7cad 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -52,7 +52,9 @@ from .forms import ( RadiusKeyForm, SwitchManagementCredForm, DocumentTemplateForm, - DelDocumentTemplateForm + DelDocumentTemplateForm, + RadiusAttributeForm, + DelRadiusAttributeForm ) from .models import ( Service, @@ -69,7 +71,8 @@ from .models import ( SwitchManagementCred, RadiusOption, CotisationsOption, - DocumentTemplate + DocumentTemplate, + RadiusAttribute ) from . import models from . import forms @@ -94,6 +97,7 @@ def display_options(request): radiuskey_list = RadiusKey.objects.all() switchmanagementcred_list = SwitchManagementCred.objects.all() radiusoptions, _ = RadiusOption.objects.get_or_create() + radius_attributes = RadiusAttribute.objects.all() cotisationsoptions, _created = CotisationsOption.objects.get_or_create() document_template_list = DocumentTemplate.objects.order_by('name') @@ -114,6 +118,7 @@ def display_options(request): 'radiuskey_list' : radiuskey_list, 'switchmanagementcred_list': switchmanagementcred_list, 'radiusoptions' : radiusoptions, + 'radius_attributes' : radius_attributes, 'cotisationsoptions': cotisationsoptions, 'optionnal_templates_list': optionnal_templates_list, 'document_template_list': document_template_list, @@ -502,3 +507,54 @@ def del_document_template(request, instances): 'action_name': _("Delete"), 'title': _("Delete document template") }, 'preferences/preferences.html', request) + + +@login_required +@can_create(RadiusAttribute) +def add_radiusattribute(request): + """Create a RADIUS attribute.""" + attribute = RadiusAttributeForm(request.POST or None) + if attribute.is_valid(): + attribute.save() + messages.success(request, _("The attribute was added.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': attribute, 'action_name': _("Add a RADIUS attribute")}, + 'preferences/preferences.html', + request + ) + + +@login_required +@can_edit(RadiusAttribute) +def edit_radiusattribute(request, radiusattribute_instance, **_kwargs): + """Edit a RADIUS attribute.""" + attribute = RadiusAttributeForm( + request.POST or None, + instance=radiusattribute_instance + ) + if attribute.is_valid(): + attribute.save() + messages.success(request, _("The attribute was edited.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': attribute, 'action_name': _("Edit")}, + 'preferences/preferences.html', + request + ) + +@login_required +@can_delete(RadiusAttribute) +def del_radiusattribute(request, radiusattribute_instance, **_kwargs): + """Delete a RADIUS attribute.""" + if request.method == "POST": + radiusattribute_instance.delete() + messages.success(request, _("The attribute was deleted.")) + return redirect(reverse('preferences:display-options')) + return form( + {'objet': radiusattribute_instance, 'objet_name': 'attribute'}, + 'preferences/delete.html', + request + ) + + diff --git a/re2o/settings_local.example.py b/re2o/settings_local.example.py index bb0fd21e..80c109c4 100644 --- a/re2o/settings_local.example.py +++ b/re2o/settings_local.example.py @@ -1,3 +1,4 @@ +# coding: utf-8 # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. diff --git a/topologie/models.py b/topologie/models.py index 250e52a9..235b3885 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -400,17 +400,17 @@ class Switch(AclMixin, Machine): def profile_type_or_nothing(self, profile_type): """Return the profile for a profile_type of this switch - + If exists, returns the defined default profile for a profile type on the dormitory which the switch belongs - + Otherwise, returns the nothing profile""" profile_queryset = PortProfile.objects.filter(profil_default=profile_type) if self.get_dormitory: port_profile = profile_queryset.filter(on_dormitory=self.get_dormitory).first() or profile_queryset.first() else: port_profile = profile_queryset.first() - return port_profile or Switch.nothing_profile + return port_profile or Switch.nothing_profile() @cached_property def default_uplink_profile(self): @@ -628,7 +628,7 @@ class Building(AclMixin, RevMixin, models.Model): @cached_property def cached_name(self): - return self.get_name() + return self.get_name() def __str__(self): return self.cached_name @@ -712,7 +712,7 @@ class Port(AclMixin, RevMixin, models.Model): def get_port_profile(self): """Return the config profil for this port :returns: the profile of self (port) - + If is defined a custom profile, returns it elIf a default profile is defined for its dormitory, returns it Else, returns the global default profil @@ -730,7 +730,7 @@ class Port(AclMixin, RevMixin, models.Model): elif self.room: return self.switch.default_room_profile else: - return Switch.nothing_profile + return Switch.nothing_profile() @classmethod def get_instance(cls, portid, *_args, **kwargs):