Commit 06b9f751 authored by chapeau's avatar chapeau
Browse files

Merge branch 'dev' of https://gitlab.federez.net/re2o/re2o into new_radius_api

parents 5315a896 4407d92a
Pipeline #3165 failed with stage
in 24 seconds
# Re2o 2.9
## Install steps
To install the latest version of Re2o, checkout the [dedicated wiki entry](https://gitlab.federez.net/re2o/re2o/-/wikis/User-Documentation/Quick-Start#update-re2o).
## Post-install steps
### MR 531: FreeRADIUS Python3 backend
On the Radius server, add `buster-backports` to your `/etc/apt/sources.list`:
```bash
echo "deb http://deb.debian.org/debian buster-backports main contrib" >> /etc/apt/sources.list
```
**Note:** If you are running Debian Bullseye, the package should already be available without going through backports.
Then install the new required packages:
```bash
apt update
apt install -t buster-backports freeradius
cat apt_requirements_radius.txt | xargs sudo apt -y install
```
### MR 582: Autocomplete light
On the Re2o server, install the new dependency and run `collectstatic`:
```bash
sudo pip3 install -r pip_requirements.txt
python3 manage.py collectstatic
```
### MR 589: Move ldap to optional app
Add `ldap_sync` to your optional apps in your local settings if you want to keep using the LDAP synchronisation.
### Final steps
As usual, run the following commands after updating:
```bash
python3 manage.py migrate
sudo service apache2 reload
```
## New features
Here is a list of noteworthy features brought by this update:
* [!488](https://gitlab.federez.net/re2o/re2o/-/merge_requests/488): Use `+` in searches to combine keywords (e.g. `John+Doe`).
* [!495](https://gitlab.federez.net/re2o/re2o/-/merge_requests/495): Add optional behavior allowing users to override another user's room, if that user is no longer active.
* [!496](https://gitlab.federez.net/re2o/re2o/-/merge_requests/496): Add option to allow users to choose their password during account creation. They will have to separately confirm their email address.
* [!504](https://gitlab.federez.net/re2o/re2o/-/merge_requests/504): Add setting to change the minimum password length.
* [!507](https://gitlab.federez.net/re2o/re2o/-/merge_requests/507): New form for editing lists of rights that should make everyone happier.
* [!512](https://gitlab.federez.net/re2o/re2o/-/merge_requests/512): Add ability to comment on tickets.
* [!513](https://gitlab.federez.net/re2o/re2o/-/merge_requests/513): IP and MAC address history (`Statistics > Machine history` tab) which also works for deleted interfaces. Uses already existing history so events before the upgrade are taken into account.
* [!516](https://gitlab.federez.net/re2o/re2o/-/merge_requests/516): Detailed events in history views (e.g. show `old_email -> new_email`).
* [!519](https://gitlab.federez.net/re2o/re2o/-/merge_requests/519): Add ability to filter event logs (e.g. to show all the subscriptions added by an admin).
* [!569](https://gitlab.federez.net/re2o/re2o/-/merge_requests/569): Refactor navbar to make menu navigation easier.
* [!569](https://gitlab.federez.net/re2o/re2o/-/merge_requests/569): Add ability to install custom themes.
* [!578](https://gitlab.federez.net/re2o/re2o/-/merge_requests/578) : Migrations squashed to ease the installation process.
* [!582](https://gitlab.federez.net/re2o/re2o/-/merge_requests/582): Improve autocomplete fields so they load faster and have a clearer behavior (no more entering a value without clicking and thinking it was taken into account).
* [!589](https://gitlab.federez.net/re2o/re2o/-/merge_requests/589): Move LDAP to a separate optional app.
* Plenty of bux fixes.
You can view the full list of closed issues [here](https://gitlab.federez.net/re2o/re2o/-/issues?scope=all&state=all&milestone_title=Re2o 2.9).
# Before Re2o 2.9
## MR 160: Datepicker
Install libjs-jquery libjs-jquery-ui libjs-jquery-timepicker libjs-bootstrap javascript-common
......@@ -21,7 +89,6 @@ rm static_files/js/jquery-2.2.4.min.js
rm static/css/jquery-ui-timepicker-addon.css
```
## MR 159: Graph topo & MR 164: branche de création de graph
Add a graph of the network topology
......@@ -34,7 +101,6 @@ Create the *media/images* directory:
mkdir -p media/images
```
## MR 163: Fix install re2o
Refactored install_re2o.sh script.
......@@ -45,8 +111,6 @@ install_re2o.sh help
* The installation templates (LDIF files and `re2o/settings_locale.example.py`) have been changed to use `example.net` instead of `example.org` (more neutral and generic)
## MR 176: Add awesome Logo
Add the logo and fix somme issues on the navbar and home page. Only collecting the statics is needed:
......@@ -54,7 +118,6 @@ Add the logo and fix somme issues on the navbar and home page. Only collecting t
python3 manage.py collectstatic
```
## MR 172: Refactor API
Creates a new (nearly) REST API to expose all models of Re2o. See [the dedicated wiki page](https://gitlab.federez.net/federez/re2o/wikis/API/Raw-Usage) for more details on how to use it.
......@@ -75,7 +138,6 @@ OPTIONAL_APPS = (
)
```
## MR 177: Add django-debug-toolbar support
Add the possibility to enable `django-debug-toolbar` in debug mode. First install the APT package:
......
......@@ -31,31 +31,6 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
def _create_api_permission():
"""Creates the 'use_api' permission if not created.
The 'use_api' is a fake permission in the sense it is not associated with an
existing model and this ensure the permission is created every time this file
is imported.
"""
api_content_type, created = ContentType.objects.get_or_create(
app_label=settings.API_CONTENT_TYPE_APP_LABEL,
model=settings.API_CONTENT_TYPE_MODEL,
)
if created:
api_content_type.save()
api_permission, created = Permission.objects.get_or_create(
name=settings.API_PERMISSION_NAME,
content_type=api_content_type,
codename=settings.API_PERMISSION_CODENAME,
)
if created:
api_permission.save()
_create_api_permission()
def can_view(user, *args, **kwargs):
"""Check if an user can view the application.
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from django.conf import settings
def create_api_permission(apps, schema_editor):
"""Creates the 'use_api' permission if not created.
The 'use_api' is a fake permission in the sense it is not associated with an
existing model and this ensure the permission is created.
"""
ContentType = apps.get_model("contenttypes", "ContentType")
Permission = apps.get_model("auth", "Permission")
api_content_type, created = ContentType.objects.get_or_create(
app_label=settings.API_CONTENT_TYPE_APP_LABEL,
model=settings.API_CONTENT_TYPE_MODEL,
)
if created:
api_content_type.save()
api_permission, created = Permission.objects.get_or_create(
name=settings.API_PERMISSION_NAME,
content_type=api_content_type,
codename=settings.API_PERMISSION_CODENAME,
)
if created:
api_permission.save()
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.RunPython(create_api_permission)
]
This diff is collapsed.
This diff is collapsed.
......@@ -105,8 +105,8 @@ def send_mail_voucher(invoice, request=None):
"lastname": invoice.user.surname,
"email": invoice.user.email,
"phone": invoice.user.telephone,
"date_end": invoice.get_subscription().latest("date_end").date_end_memb,
"date_begin": invoice.get_subscription().earliest("date_start").date_start_memb,
"date_end": invoice.get_subscription().latest("date_end_memb").date_end_memb,
"date_begin": invoice.get_subscription().earliest("date_start_memb").date_start_memb,
}
templatename = CotisationsOption.get_cached_value(
"voucher_template"
......
from django.contrib import admin
from .models import (
LdapUser,
LdapServiceUser,
LdapServiceUserGroup,
LdapUserGroup,
)
class LdapUserAdmin(admin.ModelAdmin):
"""LdapUser Admin view. Can't change password, manage
by User General model.
Parameters:
Django ModelAdmin: Apply on django ModelAdmin
"""
list_display = ("name", "uidNumber", "login_shell")
exclude = ("user_password", "sambat_nt_password")
search_fields = ("name",)
class LdapServiceUserAdmin(admin.ModelAdmin):
"""LdapServiceUser Admin view. Can't change password, manage
by User General model.
Parameters:
Django ModelAdmin: Apply on django ModelAdmin
"""
list_display = ("name",)
exclude = ("user_password",)
search_fields = ("name",)
class LdapUserGroupAdmin(admin.ModelAdmin):
"""LdapUserGroup Admin view.
Parameters:
Django ModelAdmin: Apply on django ModelAdmin
"""
list_display = ("name", "members", "gid")
search_fields = ("name",)
class LdapServiceUserGroupAdmin(admin.ModelAdmin):
"""LdapServiceUserGroup Admin view.
Parameters:
Django ModelAdmin: Apply on django ModelAdmin
"""
list_display = ("name",)
search_fields = ("name",)
admin.site.register(LdapUser, LdapUserAdmin)
admin.site.register(LdapUserGroup, LdapUserGroupAdmin)
admin.site.register(LdapServiceUser, LdapServiceUserAdmin)
admin.site.register(LdapServiceUserGroup, LdapServiceUserGroupAdmin)
from django.apps import AppConfig
class LdapSyncConfig(AppConfig):
name = 'ldap_sync'
# Copyright © 2018 Maël Kervella
# Copyright © 2021 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
......@@ -21,6 +22,7 @@ from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from users.models import User, ListRight
from ldap_sync.models import synchronise_user, synchronise_serviceuser, synchronise_usergroup
def split_lines(lines):
......@@ -89,9 +91,9 @@ def flush_ldap(binddn, bindpass, server, usersdn, groupsdn):
def sync_ldap():
"""Syncrhonize the whole LDAP with the DB."""
for u in User.objects.all():
u.ldap_sync()
synchronise_user(sender=User, instance=u)
for lr in ListRight.objects.all():
lr.ldap_sync()
synchronise_usergroup(sender=ListRight, instance=lr)
class Command(BaseCommand):
......
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Lara Kermarec
# Copyright © 2017 Augustin Lemesle
# Copyright © 2020 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
......@@ -19,6 +20,7 @@
from django.core.management.base import BaseCommand, CommandError
from users.models import User
from ldap_sync.models import synchronise_user
class Command(BaseCommand):
......@@ -36,5 +38,5 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
for usr in User.objects.all():
usr.ldap_sync(mac_refresh=options["full"])
for user in User.objects.all():
synchronise_user(sender=User, instance=user)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2021-01-10 16:59
from __future__ import unicode_literals
from django.db import migrations
#from django.conf import settings
import ldapdb.models.fields
#from ldap_sync.management.commands.ldap_rebuild import flush_ldap, sync_ldap
#def rebuild_ldap(apps, schema_editor):
# usersdn = settings.LDAP["base_user_dn"]
# groupsdn = settings.LDAP["base_usergroup_dn"]
# binddn = settings.DATABASES["ldap"]["USER"]
# bindpass = settings.DATABASES["ldap"]["PASSWORD"]
# server = settings.DATABASES["ldap"]["NAME"]
# flush_ldap(binddn, bindpass, server, usersdn, groupsdn)
class Migration(migrations.Migration):
initial = True
dependencies = [
('users', '0002_foreign_keys')
]
operations = [
migrations.CreateModel(
name='LdapServiceUser',
fields=[
('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)),
('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)),
('user_password', ldapdb.models.fields.CharField(blank=True, db_column='userPassword', max_length=200, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='LdapServiceUserGroup',
fields=[
('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)),
('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)),
('members', ldapdb.models.fields.ListField(blank=True, db_column='member')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='LdapUser',
fields=[
('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)),
('gid', ldapdb.models.fields.IntegerField(db_column='gidNumber')),
('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)),
('uid', ldapdb.models.fields.CharField(db_column='uid', max_length=200)),
('uidNumber', ldapdb.models.fields.IntegerField(db_column='uidNumber', unique=True)),
('sn', ldapdb.models.fields.CharField(db_column='sn', max_length=200)),
('login_shell', ldapdb.models.fields.CharField(blank=True, db_column='loginShell', max_length=200, null=True)),
('mail', ldapdb.models.fields.CharField(db_column='mail', max_length=200)),
('given_name', ldapdb.models.fields.CharField(db_column='givenName', max_length=200)),
('home_directory', ldapdb.models.fields.CharField(db_column='homeDirectory', max_length=200)),
('display_name', ldapdb.models.fields.CharField(blank=True, db_column='displayName', max_length=200, null=True)),
('dialupAccess', ldapdb.models.fields.CharField(db_column='dialupAccess', max_length=200)),
('sambaSID', ldapdb.models.fields.IntegerField(db_column='sambaSID', unique=True)),
('user_password', ldapdb.models.fields.CharField(blank=True, db_column='userPassword', max_length=200, null=True)),
('sambat_nt_password', ldapdb.models.fields.CharField(blank=True, db_column='sambaNTPassword', max_length=200, null=True)),
('macs', ldapdb.models.fields.ListField(blank=True, db_column='radiusCallingStationId', max_length=200, null=True)),
('shadowexpire', ldapdb.models.fields.CharField(blank=True, db_column='shadowExpire', max_length=200, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='LdapUserGroup',
fields=[
('dn', ldapdb.models.fields.CharField(max_length=200, serialize=False)),
('gid', ldapdb.models.fields.IntegerField(db_column='gidNumber')),
('members', ldapdb.models.fields.ListField(blank=True, db_column='memberUid')),
('name', ldapdb.models.fields.CharField(db_column='cn', max_length=200, primary_key=True, serialize=False)),
],
options={
'abstract': False,
},
),
migrations.AlterField(
model_name='ldapserviceuser',
name='dn',
field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='ldapserviceusergroup',
name='dn',
field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='ldapuser',
name='dn',
field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='ldapusergroup',
name='dn',
field=ldapdb.models.fields.CharField(max_length=200, primary_key=True, serialize=False),
),
]
import sys
from django.db import models
from django.conf import settings
from django.dispatch import receiver
from django.contrib.auth.models import Group
import ldapdb.models
import ldapdb.models.fields
import users.signals
import users.models
import machines.models
class LdapUser(ldapdb.models.Model):
"""A class representing a LdapUser in LDAP, its LDAP conterpart.
Synced from re2o django User model, (User django models),
with a copy of its attributes/fields into LDAP, so this class is a mirror
of the classic django User model.
The basedn userdn is specified in settings.
Attributes:
name: The name of this User
uid: The uid (login) for the unix user
uidNumber: Linux uid number
gid: The default gid number for this user
sn: The user "str" pseudo
login_shell: Linux shell for the user
mail: Email address contact for this user
display_name: Pretty display name for this user
dialupAccess: Boolean, True for valid membership
sambaSID: Identical id as uidNumber
user_password: SSHA hashed password of user
samba_nt_password: NTLM hashed password of user
macs: Multivalued mac address
shadowexpire: Set it to 0 to block access for this user and disabled
account
"""
# LDAP meta-data
base_dn = settings.LDAP["base_user_dn"]
object_classes = [
"inetOrgPerson",
"top",
"posixAccount",
"sambaSamAccount",
"radiusprofile",
"shadowAccount",
]
# attributes
gid = ldapdb.models.fields.IntegerField(db_column="gidNumber")
name = ldapdb.models.fields.CharField(
db_column="cn", max_length=200, primary_key=True
)
uid = ldapdb.models.fields.CharField(db_column="uid", max_length=200)
uidNumber = ldapdb.models.fields.IntegerField(db_column="uidNumber", unique=True)
sn = ldapdb.models.fields.CharField(db_column="sn", max_length=200)
login_shell = ldapdb.models.fields.CharField(
db_column="loginShell", max_length=200, blank=True, null=True
)
mail = ldapdb.models.fields.CharField(db_column="mail", max_length=200)
given_name = ldapdb.models.fields.CharField(db_column="givenName", max_length=200)
home_directory = ldapdb.models.fields.CharField(
db_column="homeDirectory", max_length=200
)
display_name = ldapdb.models.fields.CharField(
db_column="displayName", max_length=200, blank=True, null=True
)
dialupAccess = ldapdb.models.fields.CharField(db_column="dialupAccess")
sambaSID = ldapdb.models.fields.IntegerField(db_column="sambaSID", unique=True)
user_password = ldapdb.models.fields.CharField(
db_column="userPassword", max_length=200, blank=True, null=True
)
sambat_nt_password = ldapdb.models.fields.CharField(
db_column="sambaNTPassword", max_length=200, blank=True, null=True
)
macs = ldapdb.models.fields.ListField(
db_column="radiusCallingStationId", max_length=200, blank=True, null=True
)
shadowexpire = ldapdb.models.fields.CharField(
db_column="shadowExpire", blank=True, null=True
)
def __str__(self):
return self.name
def __unicode__(self):
return self.name
def save(self, *args, **kwargs):
self.sn = self.name
self.uid = self.name
self.sambaSID = self.uidNumber
super(LdapUser, self).save(*args, **kwargs)
@receiver(users.signals.synchronise, sender=users.models.User)
def synchronise_user(sender, **kwargs):
"""
Synchronise an User to the LDAP.
Args:
* sender : The model class.
* instance : The actual instance being synchronised.
* base : Default `True`. When `True`, synchronise basic attributes.
* access_refresh : Default `True`. When `True`, synchronise the access time.
* mac_refresh : Default `True`. When True, synchronise the list of mac addresses.
* group_refresh: Default `False`. When `True` synchronise the groups of the instance.
"""
base=kwargs.get('base', True)
access_refresh=kwargs.get('access_refresh', True)
mac_refresh=kwargs.get('mac_refresh', True )
group_refresh=kwargs.get('group_refresh', False)
user=kwargs["instance"]
if sys.version_info[0] >= 3 and (
user.state == user.STATE_ACTIVE
or user.state == user.STATE_ARCHIVE
or user.state == user.STATE_DISABLED
):
user.refresh_from_db()
try:
user_ldap = LdapUser.objects.get(uidNumber=user.uid_number)
except LdapUser.DoesNotExist:
user_ldap = LdapUser(uidNumber=user.uid_number)
base = True
access_refresh = True
mac_refresh = True
if base:
user_ldap.name = user.pseudo
user_ldap.sn = user.pseudo
user_ldap.dialupAccess = str(user.has_access())
user_ldap.home_directory = user.home_directory
user_ldap.mail = user.get_mail
user_ldap.given_name = (
user.surname.lower() + "_" + user.name.lower()[:3]
)
user_ldap.gid = settings.LDAP["user_gid"]
if "{SSHA}" in user.password or "{SMD5}" in user.password:
# We remove the extra $ added at import from ldap
user_ldap.user_password = user.password[:6] + user.password[7:]
elif "{crypt}" in user.password:
# depending on the length, we need to remove or not a $
if len(user.password) == 41:
user_ldap.user_password = user.password
else:
user_ldap.user_password = user.password[:7] + user.password[8:]
user_ldap.sambat_nt_password = user.pwd_ntlm.upper()
if user.get_shell:
user_ldap.login_shell = str(user.get_shell)
user_ldap.shadowexpire = user.get_shadow_expire
if access_refresh:
user_ldap.dialupAccess = str(user.has_access())
if mac_refresh:
user_ldap.macs = [
str(mac)
for mac in machines.models.Interface.objects.filter(machine__user=user)
.values_list("mac_address", flat=True)
.distinct()
]
if group_refresh:
# Need to refresh all groups because we don't know which groups
# were updated during edition of groups and the user may no longer
# be part of the updated group (case of group removal)
for group in Group.objects.all():
if hasattr(group, "listright"):
synchronise_usergroup(users.models.ListRight, instance=group.listright)
user_ldap.save()
@receiver(users.signals.remove, sender=users.models.User)
def remove_user(sender, **kwargs):
user = kwargs["instance"]
try:
user_ldap = LdapUser.objects.get(name=user.pseudo)
user_ldap.delete()