8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-23 07:23:46 +00:00
This commit is contained in:
Hugo Levy-Falk 2020-04-23 22:32:01 +02:00 committed by Gabriel Detraz
parent e7795a775c
commit f6b2225eb8
13 changed files with 380 additions and 36 deletions

View file

@ -48,6 +48,7 @@ from .models import (
LdapServiceUser,
LdapServiceUserGroup,
LdapUserGroup,
SSHKey,
)
from .forms import (
UserChangeForm,
@ -130,6 +131,12 @@ class WhitelistAdmin(VersionAdmin):
pass
class SSHKeyAdmin(VersionAdmin):
"""SSHKey model for admin."""
pass
class UserAdmin(VersionAdmin, BaseUserAdmin):
"""Gestion d'un user : modification des champs perso, mot de passe, etc"""
@ -224,6 +231,7 @@ admin.site.register(Ban, BanAdmin)
admin.site.register(EMailAddress, EMailAddressAdmin)
admin.site.register(Whitelist, WhitelistAdmin)
admin.site.register(Request, RequestAdmin)
admin.site.register(SSHKey, SSHKeyAdmin)
# Now register the new UserAdmin...
admin.site.unregister(User)
admin.site.unregister(ServiceUser)

View file

@ -24,6 +24,7 @@ from rest_framework import serializers
import users.models as users
from api.serializers import NamespacedHRField, NamespacedHIField, NamespacedHMSerializer
class UserSerializer(NamespacedHMSerializer):
"""Serialize `users.models.User` objects.
"""
@ -210,7 +211,7 @@ class EMailAddressSerializer(NamespacedHMSerializer):
class Meta:
model = users.EMailAddress
fields = ("user", "local_part", "complete_email_address", "api_url")
class LocalEmailUsersSerializer(NamespacedHMSerializer):
email_address = EMailAddressSerializer(read_only=True, many=True)
@ -241,4 +242,12 @@ class MailingSerializer(ClubSerializer):
admins = MailingMemberSerializer(source="administrators", many=True)
class Meta(ClubSerializer.Meta):
fields = ("name", "members", "admins")
fields = ("name", "members", "admins")
class SSHKeySerializer(NamespacedHMSerializer):
"""Serialize an SSHKey."""
class Meta:
model = users.SSHKey
fields = "__all__"

View file

@ -34,16 +34,16 @@ urls_viewset = [
(r"users/shell", views.ShellViewSet, "shell"),
(r"users/ban", views.BanViewSet, None),
(r"users/whitelist", views.WhitelistViewSet, None),
(r"users/emailaddress", views.EMailAddressViewSet, None)
(r"users/emailaddress", views.EMailAddressViewSet, None),
(r"users/sshkey", views.SSHKeyViewSet, None),
]
urls_view = [
(r"users/localemail", views.LocalEmailUsersView),
(r"users/mailing-standard", views.StandardMailingView),
(r"users/mailing-club", views.ClubMailingView),
# Deprecated
(r"localemail/users", views.LocalEmailUsersView),
(r"mailing/standard", views.StandardMailingView),
(r"mailing/club", views.ClubMailingView),
]
]

View file

@ -169,7 +169,7 @@ class StandardMailingView(views.APIView):
adherents_data = serializers.MailingMemberSerializer(
all_has_access(), many=True
).data
data = [{"name": "adherents", "members": adherents_data}]
groups = Group.objects.all()
for group in groups:
@ -189,4 +189,12 @@ class ClubMailingView(generics.ListAPIView):
"""
queryset = users.Club.objects.all()
serializer_class = serializers.MailingSerializer
serializer_class = serializers.MailingSerializer
class SSHKeyViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.SSHKey` objects.
"""
queryset = users.SSHKey.objects.all()
serializer_class = serializers.SSHKeySerializer

View file

@ -38,7 +38,10 @@ from __future__ import unicode_literals
from django import forms
from django.forms import ModelForm, Form
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth.password_validation import validate_password, password_validators_help_text_html
from django.contrib.auth.password_validation import (
validate_password,
password_validators_help_text_html,
)
from django.core.validators import MinLengthValidator
from django.utils import timezone
from django.utils.functional import lazy
@ -69,6 +72,7 @@ from .models import (
Ban,
Adherent,
Club,
SSHKey,
)
@ -84,7 +88,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
label=_("New password"),
max_length=255,
widget=forms.PasswordInput,
help_text=password_validators_help_text_html()
help_text=password_validators_help_text_html(),
)
passwd2 = forms.CharField(
label=_("New password confirmation"),
@ -133,12 +137,10 @@ class UserCreationForm(FormRevMixin, forms.ModelForm):
label=_("Password"),
widget=forms.PasswordInput,
max_length=255,
help_text=password_validators_help_text_html()
help_text=password_validators_help_text_html(),
)
password2 = forms.CharField(
label=_("Password confirmation"),
widget=forms.PasswordInput,
max_length=255,
label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255,
)
is_admin = forms.BooleanField(label=_("Is admin"))
@ -287,9 +289,7 @@ class MassArchiveForm(forms.Form):
date = forms.DateTimeField(help_text="%d/%m/%y")
full_archive = forms.BooleanField(
label=_(
"Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"
),
label=_("Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"),
initial=False,
required=False,
)
@ -380,6 +380,7 @@ class AdherentCreationForm(AdherentForm):
AdherentForm auquel on ajoute une checkbox afin d'éviter les
doublons d'utilisateurs et, optionnellement,
un champ mot de passe"""
# Champ pour choisir si un lien est envoyé par mail pour le mot de passe
init_password_by_mail_info = _(
"If this options is set, you will receive a link to set"
@ -392,9 +393,7 @@ class AdherentCreationForm(AdherentForm):
)
init_password_by_mail = forms.BooleanField(
help_text=init_password_by_mail_info,
required=False,
initial=True
help_text=init_password_by_mail_info, required=False, initial=True
)
init_password_by_mail.label = _("Send password reset link by email.")
@ -405,7 +404,7 @@ class AdherentCreationForm(AdherentForm):
label=_("Password"),
widget=forms.PasswordInput,
max_length=255,
help_text=password_validators_help_text_html()
help_text=password_validators_help_text_html(),
)
password2 = forms.CharField(
required=False,
@ -482,8 +481,12 @@ class AdherentCreationForm(AdherentForm):
# Save the provided password in hashed format
user = super(AdherentForm, self).save(commit=False)
is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation")
set_passwd = is_set_password_allowed and not self.cleaned_data.get("init_password_by_mail")
is_set_password_allowed = OptionalUser.get_cached_value(
"allow_set_password_during_user_creation"
)
set_passwd = is_set_password_allowed and not self.cleaned_data.get(
"init_password_by_mail"
)
if set_passwd:
user.set_password(self.cleaned_data["password1"])
@ -886,3 +889,15 @@ class InitialRegisterForm(forms.Form):
if self.cleaned_data["register_machine"]:
if self.mac_address and self.nas_type:
self.user.autoregister_machine(self.mac_address, self.nas_type)
class SSHKeyForm(FormRevMixin, ModelForm):
"""Create or edit an SSHKey"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
super(SSHKeyForm, self).__init__(*args, prefix=prefix, **kwargs)
class Meta:
model = SSHKey
exclude = ["user"]

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-23 16:04
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import ldapdb.models.fields
import re2o.mixins
class Migration(migrations.Migration):
dependencies = [
("users", "0091_auto_20200423_1256"),
]
operations = [
migrations.CreateModel(
name="SSHKey",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("key", models.TextField(blank=True, verbose_name="Public ssh key")),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "SSH key",
"verbose_name_plural": "SSH keys",
"permissions": (("view_sshkey", "Can view an SSHKey object"),),
},
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model),
),
migrations.AddField(
model_name="ldapuser",
name="ssh_keys",
field=ldapdb.models.fields.ListField(
blank=True, db_column="sshkeys", null=True
),
),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-23 18:28
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("users", "0092_auto_20200423_1804"),
]
operations = [
migrations.RenameField(
model_name="ldapuser", old_name="ssh_keys", new_name="sshkeys",
),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-23 18:30
from __future__ import unicode_literals
from django.db import migrations
import ldapdb.models.fields
class Migration(migrations.Migration):
dependencies = [
("users", "0093_auto_20200423_2028"),
]
operations = [
migrations.AlterField(
model_name="ldapuser",
name="sshkeys",
field=ldapdb.models.fields.ListField(
blank=True, db_column="sshkeys", max_length=200, null=True
),
),
]

View file

@ -105,7 +105,7 @@ def linux_user_validator(login):
pas les contraintes unix (maj, min, chiffres ou tiret)"""
if not linux_user_check(login):
raise forms.ValidationError(
_("The username \"%(label)s\" contains forbidden characters."),
_('The username "%(label)s" contains forbidden characters.'),
params={"label": login},
)
@ -405,7 +405,10 @@ class User(
@cached_property
def get_shadow_expire(self):
"""Return the shadow_expire value for the user"""
if self.state == self.STATE_DISABLED or self.email_state == self.EMAIL_STATE_UNVERIFIED:
if (
self.state == self.STATE_DISABLED
or self.email_state == self.EMAIL_STATE_UNVERIFIED
):
return str(0)
else:
return None
@ -670,7 +673,12 @@ class User(
self.full_archive()
def ldap_sync(
self, base=True, access_refresh=True, mac_refresh=True, group_refresh=False
self,
base=True,
access_refresh=True,
mac_refresh=True,
group_refresh=False,
sshkeys_refresh=False,
):
""" Synchronisation du ldap. Synchronise dans le ldap les attributs de
self
@ -734,6 +742,10 @@ class User(
for group in Group.objects.all():
if hasattr(group, "listright"):
group.listright.ldap_sync()
if sshkeys_refresh:
user_ldap.sshkeys = [
str(key.key) for key in SSHKey.objects.filter(user=self)
]
user_ldap.save()
def ldap_del(self):
@ -1051,7 +1063,7 @@ class User(
False,
_(
"Impossible to edit the organisation's"
" user without the \"change_all_users\" right."
' user without the "change_all_users" right.'
),
("users.change_all_users",),
)
@ -1120,7 +1132,8 @@ class User(
if not (
(
self.pk == user_request.pk
and OptionalUser.get_cached_value("self_room_policy") != OptionalUser.DISABLED
and OptionalUser.get_cached_value("self_room_policy")
!= OptionalUser.DISABLED
)
or user_request.has_perm("users.change_user")
):
@ -1263,7 +1276,7 @@ class User(
can = user_request.is_superuser
return (
can,
_("\"superuser\" right required to edit the superuser flag.")
_('"superuser" right required to edit the superuser flag.')
if not can
else None,
[],
@ -1357,9 +1370,7 @@ class User(
# Allow empty emails only if the user had an empty email before
is_created = not self.pk
if not self.email and (self.__original_email or is_created):
raise forms.ValidationError(
_("Email field cannot be empty.")
)
raise forms.ValidationError(_("Email field cannot be empty."))
self.email = self.email.lower()
@ -1432,14 +1443,12 @@ class Adherent(User):
a user or if the `options.all_can_create` is set.
"""
if not user_request.is_authenticated:
if not OptionalUser.get_cached_value(
"self_adhesion"
):
if not OptionalUser.get_cached_value("self_adhesion"):
return False, _("Self registration is disabled."), None
else:
return True, None, None
else:
if OptionalUser.get_cached_value("all_can_create_adherent"):
if OptionalUser.get_cached_value("all_can_create_adherent"):
return True, None, None
else:
can = user_request.has_perm("users.add_user")
@ -2000,6 +2009,9 @@ class LdapUser(ldapdb.models.Model):
shadowexpire = ldapdb.models.fields.CharField(
db_column="shadowExpire", blank=True, null=True
)
sshkeys = ldapdb.models.fields.ListField(
db_column="sshkeys", max_length=200, blank=True, null=True
)
def __str__(self):
return self.name
@ -2248,3 +2260,53 @@ class EMailAddress(RevMixin, AclMixin, models.Model):
if result:
raise ValidationError(reason)
super(EMailAddress, self).clean(*args, **kwargs)
class SSHKey(RevMixin, AclMixin, models.Model):
"""Represents an SSH public key belonging to a user."""
key = models.TextField(blank=True, verbose_name=_("Public ssh key"))
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
permissions = (("view_sshkey", _("Can view an SSHKey object")),)
verbose_name = _("SSH key")
verbose_name_plural = _("SSH keys")
def can_edit(self, user_request, *_args, **_kwargs):
"""Check if a user can edit the SSH key
Args:
user_request: The user who wants to edit the object.
Returns:
a message and a boolean which is True if the user can edit
the local email account.
"""
if self.user == user_request or user_request.has_perm("users.edit_sshkey"):
return True, None, None
return (
False,
_("You don't have the right to edit another user's SSHKey."),
("users.edit_sshkey",),
)
@receiver(post_save, sender=SSHKey)
def sshkey_post_save(**kwargs):
"""Sync LDAP record for user when SSHKey is saved."""
key = kwargs["instance"]
is_created = kwargs["created"]
user = key.user
user.ldap_sync(
base=False, access_refresh=False, mac_refresh=False, sshkeys_refresh=True
)
@receiver(post_delete, sender=SSHKey)
def sshkey_post_delete(**kwargs):
"""Sync LDAP record for user when SSHKey is deleted"""
user = kwargs["instance"].user
user.ldap_sync(
base=False, access_refresh=False, mac_refresh=False, sshkeys_refresh=True
)

View file

@ -0,0 +1,60 @@
{% 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 © 2017 Gabriel Détraz
Copyright © 2017 Lara Kermarec
Copyright © 2017 Augustin Lemesle
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 %}
{% if sshkeys.paginator %}
{% include 'pagination.html' with list=sshkeys %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>{% trans "SSH key" %}</th>
<th></th>
</tr>
</thead>
{% for sshkey in sshkeys %}
<td>{{ sshkey.key }}</td>
<td class="text-right">
{% can_delete sshkey %}
{% include 'buttons/suppr.html' with href='users:del-sshkey' id=sshkey.id %}
{% acl_end %}
{% history_button sshkey %}
{% can_edit sshkey %}
{% include 'buttons/edit.html' with href='users:edit-sshkey' id=sshkey.id %}
{% acl_end %}
</td>
</tr>
{% endfor %}
</table>
{% if sshkey.paginator %}
{% include 'pagination.html' with list=sshkey %}
{% endif %}

View file

@ -562,6 +562,34 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading clearfix profil" data-parent="#accordion" data-toggle="collapse" data-target="#sshkeys">
<h3 class="panel-title pull-left">
<i class="fa fa-key"></i>
{% trans "SSH keys" %}
</h3>
</div>
<div id="sshkeys" class="panel-collapse collapse">
<div class="panel-body">
{% can_edit users %}
{% can_create SSHKey %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-sshkey' users.id %}">
<i class="fa fa-key"></i>
{% trans "Add an SSH key" %}
</a>
{% acl_end %}
{% acl_end %}
</div>
<div class="panel-body">
{% if sshkeys %}
{% include 'users/aff_sshkeys.html' with sshkeys=sshkeys %}
{% else %}
<p>{% trans "No SSH key" %}</p>
{% endif %}
</div>
</div>
</div>
{% for template in optionnal_templates_list %}
{{ template }}
{% endfor %}

View file

@ -44,7 +44,11 @@ urlpatterns = [
url(r"^state/(?P<userid>[0-9]+)$", views.state, name="state"),
url(r"^groups/(?P<userid>[0-9]+)$", views.groups, name="groups"),
url(r"^password/(?P<userid>[0-9]+)$", views.password, name="password"),
url(r"^confirm_email/(?P<userid>[0-9]+)$", views.resend_confirmation_email, name="resend-confirmation-email"),
url(
r"^confirm_email/(?P<userid>[0-9]+)$",
views.resend_confirmation_email,
name="resend-confirmation-email",
),
url(
r"^del_group/(?P<userid>[0-9]+)/(?P<listrightid>[0-9]+)$",
views.del_group,
@ -127,4 +131,7 @@ urlpatterns = [
url(r"^$", views.index, name="index"),
url(r"^index_clubs/$", views.index_clubs, name="index-clubs"),
url(r"^initial_register/$", views.initial_register, name="initial-register"),
url(r"^add_sshkey/(?P<userid>[0-9]+)$", views.add_sshkey, name="add-sshkey",),
url(r"^edit_sshkey/(?P<sshkeyid>[0-9]+)$", views.edit_sshkey, name="edit-sshkey",),
url(r"^del_sshkey/(?P<sshkeyid>[0-9]+)$", views.del_sshkey, name="del-sshkey",),
]

View file

@ -86,6 +86,7 @@ from .models import (
Club,
ListShell,
EMailAddress,
SSHKey,
)
from .forms import (
BanForm,
@ -110,6 +111,7 @@ from .forms import (
ClubAdminandMembersForm,
GroupForm,
InitialRegisterForm,
SSHKeyForm,
)
@ -932,6 +934,7 @@ def profil(request, users, **_kwargs):
request.GET.get("order"),
SortTable.USERS_INDEX_WHITE,
)
sshkeys = users.sshkey_set.all()
try:
balance = find_payment_method(Paiement.objects.get(is_balance=True))
except Paiement.DoesNotExist:
@ -956,6 +959,7 @@ def profil(request, users, **_kwargs):
"local_email_accounts_enabled": (
OptionalUser.objects.first().local_email_accounts_enabled
),
"sshkeys": sshkeys,
},
)
@ -1101,3 +1105,51 @@ def initial_register(request):
request,
)
@login_required
@can_create(SSHKey)
@can_edit(User)
def add_sshkey(request, user, userid):
"""Create an SSHKey for the given user."""
sshkey_instance = SSHKey(user=user)
sshkey = SSHKeyForm(request.POST or None, instance=sshkey_instance)
if sshkey.is_valid():
sshkey.save()
messages.success(request, _("The SSH key was added."))
return redirect(reverse("users:profil", kwargs={"userid": str(userid)}))
return form(
{"userform": sshkey, "action_name": _("Add")}, "users/user.html", request
)
@login_required
@can_edit(SSHKey)
def edit_sshkey(request, sshkey_instance, **_kwargs):
"""Edit an SSHKey for the given user."""
sshkey = SSHKeyForm(request.POST or None, instance=sshkey_instance)
sshkey.request = request
if sshkey.is_valid():
if sshkey.changed_data:
sshkey.save()
messages.success(request, _("The SSH Key was edited."))
return redirect(reverse("users:profil", kwargs={"userid": str(userid)}))
return form(
{"userform": sshkey, "action_name": _("Edit")}, "users/user.html", request
)
@login_required
@can_delete(SSHKey)
def del_sshkey(request, sshkey, **_kwargs):
"""Delete SSH key."""
if request.method == "POST":
sshkey.delete()
messages.success(request, _("The SSH key was deleted."))
return redirect(reverse("users:profil", kwargs={"userid": str(sshkey.user.id)}))
return form(
{"objet": sshkey, "objet_name": _("SSH key")}, "users/delete.html", request
)