Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • forge/apps/forgeid
1 result
Select Git revision
Show changes
Commits on Source (9)
Showing
with 76 additions and 562 deletions
default:
before_script:
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:19.03.1
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:26.1.3
include:
- template: SAST.gitlab-ci.yml
- template: Container-Scanning.gitlab-ci.yml
- template: Code-Quality.gitlab-ci.yml
- template: Security/License-Scanning.gitlab-ci.yml
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Container-Scanning.gitlab-ci.yml
- template: Jobs/Code-Quality.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
sast:
stage: security
......@@ -21,13 +21,11 @@ container_scanning:
variables:
DOCKER_SERVICE: localhost
code_quality:
stage: qa
dependency_scanning:
stage: security
license_scanning:
code_quality:
stage: qa
variables:
LM_PYTHON_VERSION: 3
stages:
- build
......@@ -66,6 +64,7 @@ lint:
artifacts:
reports:
junit: prospector-output.xml
allow_failure: true
script:
- prospector --profile ci > prospector-output.xml
black:
......@@ -103,6 +102,7 @@ pages:
- cd docker/
- ./gen_secrets.sh
- cd ..
- poetry install --only docs
script:
- sphinx-apidoc -M -e -o docs/source/apidoc/ . '*migrations/*'
- make -C docs/ html
......@@ -113,10 +113,10 @@ test:
needs:
- build
image:
name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker/compose:1.25.0-alpine
name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:26.1.3
entrypoint: [""]
services:
- docker:20.10.14-dind
- docker:26.1.3-dind
tags:
- docker
- privileged
......@@ -126,6 +126,9 @@ test:
artifacts:
reports:
junit: test-reports/*.xml
coverage_report:
coverage_format: cobertura
path: artifacts/coverage.xml
paths:
- artifacts/
when: always
......@@ -139,17 +142,17 @@ test:
- ./gen_secrets.sh
- docker pull $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
- docker tag $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA intranet_dev
- docker-compose pull --ignore-pull-failures
- docker compose pull --ignore-pull-failures
- export COMPOSE_PARALLEL_LIMIT=1000
- docker network prune
- docker-compose -p "$CI_CONCURRENT_ID" up -d s3_dev ldap_dev ldap_sasl_dev db_dev
- docker compose -p "$CI_CONCURRENT_ID" up -d s3_dev ldap_dev ldap_sasl_dev db_dev
- sleep 30
- docker-compose -p "$CI_CONCURRENT_ID" up -d
- docker-compose -p "$CI_CONCURRENT_ID" exec -T intranet_dev coverage run --source='.' ./manage.py test
- docker-compose -p "$CI_CONCURRENT_ID" exec -T intranet_dev coverage report
- docker-compose -p "$CI_CONCURRENT_ID" exec -T intranet_dev coverage html -d /app/artifacts/coverage/
- docker compose -p "$CI_CONCURRENT_ID" up -d
- docker compose -p "$CI_CONCURRENT_ID" exec -T intranet_dev coverage run --source='.' ./manage.py test
- docker compose -p "$CI_CONCURRENT_ID" exec -T intranet_dev coverage report
- docker compose -p "$CI_CONCURRENT_ID" exec -T intranet_dev coverage xml -o artifacts/coverage.xml
- cd ..
after_script:
- cd docker/
- docker-compose -p "$CI_CONCURRENT_ID" logs -t --no-color > ../artifacts/docker-compose.log
- docker-compose -p "$CI_CONCURRENT_ID" down -v
- docker compose -p "$CI_CONCURRENT_ID" logs -t --no-color > ../artifacts/docker-compose.log
- docker compose -p "$CI_CONCURRENT_ID" down -v
......@@ -32,9 +32,8 @@ class AuthGroupsClientsMappingAdmin(admin.ModelAdmin):
@admin.register(models.OIDCClientExtension)
class OIDCClientExtensionAdmin(admin.ModelAdmin):
list_display = ("client", "slug", "is_legacy")
list_display = ("client", "slug")
list_display_links = list_display
list_filter = ("is_legacy",)
search_fields = (
"client__name",
"client___redirect_uris",
......@@ -44,7 +43,6 @@ class OIDCClientExtensionAdmin(admin.ModelAdmin):
autocomplete_fields = ("client",)
readonly_fields = ("groups_clients_mapping",)
# pylint: disable=arguments-differ
def has_add_permission(self, *_args, **_kwargs):
return False
......@@ -67,14 +65,11 @@ class ConnectionLogEntryAdmin(admin.ModelAdmin):
"client__name",
)
# pylint: disable=arguments-differ
def has_add_permission(self, *_args, **_kwargs):
return False
# pylint: disable=arguments-differ
def has_change_permission(self, *_args, **_kwargs):
return False
# pylint: disable=arguments-differ
def has_delete_permission(self, *_args, **_kwargs):
return False
# Generated by Django 4.2.13 on 2024-05-30 21:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("cri_auth", "0009_auto_20210802_0248"),
]
operations = [
migrations.RemoveField(
model_name="oidcclientextension",
name="is_legacy",
),
]
......@@ -262,14 +262,6 @@ class AuthMethodMixin(sync.SyncedModelMixin, models.Model):
class OIDCClientExtension(AuthMethodMixin):
client = models.OneToOneField(OIDCClient, on_delete=models.CASCADE)
is_legacy = models.BooleanField(
default=False,
help_text=(
"If enabled, the claims returned by the epita scope will match the old "
"OIDC API. Do not use for new OIDC clients."
),
)
class Meta:
verbose_name = "OIDC client extension"
......@@ -339,7 +331,6 @@ class ConnectionLogEntry(models.Model):
def __str__(self):
return f"{self.created_at} - {self.username} ({self.kind})"
# pylint: disable=arguments-differ
def save(self, *args, **kwargs):
if not self.username:
self.username = self.user.username
......
......@@ -3,7 +3,6 @@ from django.utils import timezone
from .base import CRIScope, CustomScopeClaims
from cri_models import models as cri_models
from cri_legacy import models as cri_legacy_models
from .. import models as cri_auth_models
......@@ -90,22 +89,6 @@ class EPITAScope(CRIScope):
.order_by("graduation_year")
.values_list("graduation_year", flat=True)
)
if cri_auth_models.OIDCClientExtension.from_client(self.client).is_legacy:
return {
"login": self.user.username,
"promo": max(graduation_years) if graduation_years else None,
"uid_number": self.user.uid,
"roles": list(
cri_legacy_models.LegacyRole.objects.filter(
members=self.user
).values_list("name", flat=True)
),
"class_groups": list(
cri_legacy_models.LegacyClassGroup.objects.filter(
members=self.user
).values_list("name", flat=True)
),
}
return {
"uid": self.user.uid,
"gid": self.user.primary_group.gid,
......
......@@ -151,7 +151,7 @@ class Card(models.Model):
card.save()
return card
def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
def delete(self, *args, **kwargs):
self.picture.delete()
return super().delete(*args, **kwargs)
......@@ -195,7 +195,6 @@ class CardTask(cri_tasks_models.CRITask):
owner=owner,
).save()
# pylint: disable=arguments-differ
@classmethod
def run(cls, *_args, user_pk, template_pk, values, **_kwargs):
user = get_user_model().objects.get(pk=user_pk)
......@@ -209,7 +208,7 @@ class CardTask(cri_tasks_models.CRITask):
class CardBundle(models.Model):
zip_bundle = models.FileField()
def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
def delete(self, *args, **kwargs):
self.zip_bundle.delete()
return super().delete(*args, **kwargs)
......@@ -223,7 +222,6 @@ class CardBundleTask(cri_tasks_models.CRITask):
template = get_template("cri_cards/_cardbundletask_result.html")
return template.render(context={"cardbundletask": self})
# pylint: disable=arguments-differ
@classmethod
def run(cls, *_args, cardtask_group: str, card_count: int, **_kwargs):
cards: list[Card] = result_group(cardtask_group, count=card_count)
......
......@@ -34,7 +34,7 @@ class CardIssueFormView(
del kwargs["files"]
return kwargs
def form_valid(self, formset): # pylint: disable=arguments-differ
def form_valid(self, formset): # pylint: disable=arguments-renamed
if len(formset) == 1:
try:
card = formset[0].issue()
......
......@@ -32,7 +32,7 @@ class HeaderShortcut(models.Model):
return 0
return last_shortcut.order + 1
def save(self, *args, **kwargs): # pylint: disable=arguments-differ
def save(self, *args, **kwargs):
if self.order is None:
self.order = self.get_next_order()
super().save(*args, **kwargs)
......
......@@ -88,12 +88,6 @@
</h5>
</div>
<div class="modal-body">
{% if userconsent.client.oidcclientextension.is_legacy %}
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-circle"></i>
This OIDC client is using the legacy interface.
</div>
{% endif %}
{% with claims=claims|pprint %}
<textarea readonly class="text-monospace w-100" rows="{{ claims.splitlines|length|add:1 }}">{{ claims }}</textarea>
{% endwith %}
......
......@@ -68,9 +68,6 @@
{% endfor %}
</td>
<td class="align-middle d-flex justify-content-end">
{% if userconsent.client.oidcclientextension.is_legacy %}
<button class="mr-1 disabled btn btn-sm btn-outline-danger" data-toggle="tooltip" data-placement="top" title="this client is using the legacy interface">Legacy</button>
{% endif %}
<span data-toggle="tooltip" data-placement="top" title="see claims values">
<button class="btn btn-sm btn-info mr-1" type="button" data-toggle="modal" data-target="#claims-{{ userconsent.pk }}">
<i class="fab fa-openid"></i>
......@@ -140,9 +137,6 @@
</td>
<td class="align-middle">
<div class="d-flex justify-content-center flex-nowrap">
{% if client.oidcclientextension.is_legacy %}
<button class=" mr-1 disabled btn btn-sm btn-outline-danger" data-toggle="tooltip" data-placement="top" title="this client is using the legacy interface">Legacy</button>
{% endif %}
{% if client.oidcclientextension.auth_groups_filter.is_restricted %}
<button class="mr-1 disabled btn btn-sm btn-outline-warning" data-toggle="tooltip" data-placement="top" title="this client only allow some users to sign-in">Restricted</button>
{% endif %}
......@@ -189,12 +183,6 @@
</h5>
</div>
<div class="modal-body">
{% if userconsent.client.oidcclientextension.is_legacy %}
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-circle"></i>
This OIDC client is using the legacy interface.
</div>
{% endif %}
{% with claims=claims|pprint %}
<textarea readonly class="text-monospace w-100" rows="{{ claims.splitlines|length|add:1 }}">{{ claims }}</textarea>
{% endwith %}
......
......@@ -90,7 +90,7 @@ class CRIUserProfileMixin:
user=criuser,
).order_by("scope", "-created_at")
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update(
{
......@@ -110,11 +110,11 @@ class CRIUserView(LoginRequiredMixin, CRIUserProfileMixin, generic.DetailView):
slug_field = "username"
slug_url_kwarg = "username"
def dispatch(self, *args, **kwargs): # pylint: disable=arguments-differ
def dispatch(self, *args, **kwargs):
self.criuser = self.get_object()
return super().dispatch(*args, **kwargs)
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
computed_memberships = (
......@@ -166,7 +166,7 @@ class CRIUserNotesView(CRIUserView):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update(
{
......@@ -227,7 +227,7 @@ class CRIUserNoteFormMixin(LoginRequiredMixin, generic.detail.SingleObjectMixin)
kwargs.update({"user": self.criuser, "author": self.request.user})
return kwargs
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["criuser"] = self.criuser
return context
......@@ -250,7 +250,7 @@ class CRIUserNoteDeleteView(LoginRequiredMixin, generic.DeleteView):
return True
return self.object.scope in self.writable_scopes
def dispatch(self, *args, **kwargs): # pylint: disable=arguments-differ
def dispatch(self, *args, **kwargs):
self.object = self.get_object()
self.criuser = self.object.user
self.writable_scopes = cri_models.CRIUserNoteScope.from_writer(
......@@ -260,7 +260,7 @@ class CRIUserNoteDeleteView(LoginRequiredMixin, generic.DeleteView):
return self.handle_no_permission()
return super().dispatch(*args, **kwargs)
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update(
{
......@@ -314,7 +314,7 @@ class CRIUserPreferencesView(
def get_object(self, queryset=None):
return self.model.objects.get(user=self.criuser)
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["criuser"] = self.criuser
return context
......@@ -444,7 +444,7 @@ class CRIUserOIDCView(LoginRequiredMixin, CRIUserProfileMixin, generic.DetailVie
self.object
)
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update(
{
......@@ -475,7 +475,7 @@ class OIDCConsentDeleteView(LoginRequiredMixin, generic.DeleteView):
userconsent = self.get_object()
return userconsent.user == user
def dispatch(self, *args, **kwargs): # pylint: disable=arguments-differ
def dispatch(self, *args, **kwargs):
if not self.is_user_allowed():
return self.handle_no_permission()
self.object = self.get_object()
......@@ -490,7 +490,7 @@ class CRIUserSSHKeysView(LoginRequiredMixin, CRIUserProfileMixin, generic.Create
model = cri_models.SSHPublicKey
form_class = forms.SSHPublicKeyForm
def get_context_data(self, *args, **kwargs): # pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
if not self.is_user_allowed():
context["form"] = None
......@@ -504,13 +504,13 @@ class CRIUserSSHKeysView(LoginRequiredMixin, CRIUserProfileMixin, generic.Create
def get_success_url(self):
return reverse("criuser_ssh_keys", kwargs={"username": self.criuser.username})
def dispatch(self, *args, **kwargs): # pylint: disable=arguments-differ
def dispatch(self, *args, **kwargs):
self.criuser = get_object_or_404(
get_user_model(), username=kwargs.get("username")
)
return super().dispatch(*args, **kwargs)
def post(self, *args, **kwargs): # pylint: disable=arguments-differ
def post(self, *args, **kwargs):
if not self.is_user_allowed():
return self.handle_no_permission()
return super().post(*args, **kwargs)
......@@ -528,7 +528,7 @@ class CRIUserSSHKeysView(LoginRequiredMixin, CRIUserProfileMixin, generic.Create
class CRIUserSSHKeysShortcutView(View):
def get(self, *args, **kwargs): # pylint: disable=unused-argument,arguments-differ
def get(self, *args, **kwargs):
username = kwargs["username"]
keys = cri_models.SSHPublicKey.objects.filter(user__username=username)
return HttpResponse(
......@@ -623,7 +623,7 @@ class SSHKeyDeleteView(LoginRequiredMixin, generic.DeleteView):
return True
return False
def dispatch(self, *args, **kwargs): # pylint: disable=arguments-differ
def dispatch(self, *args, **kwargs):
if not self.is_user_allowed():
return self.handle_no_permission()
return super().dispatch(*args, **kwargs)
......@@ -645,12 +645,11 @@ class CRIGroupView(LoginRequiredMixin, generic.DetailView):
groups = [] if user.is_anonymous else user.get_groups()
return not group.private or group in groups
def dispatch(self, *args, **kwargs): # pylint: disable=arguments-differ
def dispatch(self, *args, **kwargs):
if not self.is_user_allowed():
return self.handle_no_permission()
return super().dispatch(*args, **kwargs)
# pylint: disable=arguments-differ
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
......@@ -677,7 +676,6 @@ class SearchFormView(LoginRequiredMixin, generic.FormView):
template_name = "cri_frontend/search.html"
form_class = forms.SearchForm
# pylint: disable=arguments-differ
def get_context_data(self, *args, form=None, **kwargs):
c = super().get_context_data(*args, **kwargs)
......@@ -727,7 +725,7 @@ class ExportView(LoginRequiredMixin, generic.FormView):
def is_user_allowed(self):
return self.request.user.has_perm("cri_models.export_data")
def dispatch(self, *args, **kwargs): # pylint: disable=arguments-differ
def dispatch(self, *args, **kwargs):
if not self.is_user_allowed():
return self.handle_no_permission()
return super().dispatch(*args, **kwargs)
......@@ -814,13 +812,13 @@ def get_criphoto(request_user, username, photo_type):
class CRIUserPhoto(View):
def get(self, *args, **kwargs): # pylint: disable=unused-argument,arguments-differ
def get(self, *args, **kwargs):
username = kwargs["username"]
return get_criphoto(self.request.user, username, None)
class CRIUserPhotoThumb(View):
def get(self, *args, **kwargs): # pylint: disable=unused-argument,arguments-differ
def get(self, *args, **kwargs):
username = kwargs["username"]
return get_criphoto(
self.request.user,
......@@ -830,7 +828,7 @@ class CRIUserPhotoThumb(View):
class CRIUserPhotoSquare(View):
def get(self, *args, **kwargs): # pylint: disable=unused-argument,arguments-differ
def get(self, *args, **kwargs):
username = kwargs["username"]
return get_criphoto(
self.request.user,
......
from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage
from storages.backends.s3 import S3Storage
class PublicS3Storage(S3Boto3Storage):
class PublicS3Storage(S3Storage):
"""
This class is a S3 django-storages class. It adds a custom domain setting to
the default S3 storage class. This is useful because django-storages does
......@@ -17,5 +17,5 @@ class PublicS3Storage(S3Boto3Storage):
object_parameters = settings.AWS_PUBLIC_OBJECT_PARAMETERS
class PhotoStorage(S3Boto3Storage):
class PhotoStorage(S3Storage):
bucket_name = settings.AWS_STORAGE_PHOTOS_BUCKET_NAME
......@@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
import os
import env
import sys
from django.contrib import messages
from django.utils.functional import lazy
......@@ -57,6 +58,8 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
DATA_UPLOAD_MAX_NUMBER_FIELDS = None
TESTING = "test" in sys.argv
# Kerberos settings
SPNEGO_HOSTNAME = env.get_string("SPNEGO_HOSTNAME")
......@@ -117,6 +120,7 @@ INSTALLED_APPS = [
"import_export",
"django_q",
"crispy_forms",
"crispy_bootstrap4",
"ckeditor",
"cri_auth",
"cri_api",
......@@ -124,7 +128,6 @@ INSTALLED_APPS = [
"cri_tasks",
"cri_cards",
"cri_frontend",
"cri_legacy",
"oidc_provider", # Must stay after cri_auth
]
......@@ -133,7 +136,7 @@ MIDDLEWARE = [
"cri_intranet.middleware.ProbesMiddleware",
]
if DEBUG:
if DEBUG and not TESTING:
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
......@@ -325,7 +328,7 @@ AWS_ACCESS_KEY_ID = env.get_secret("S3_ACCESS_KEY")
AWS_SECRET_ACCESS_KEY = env.get_secret("S3_SECRET_KEY")
AWS_STORAGE_BUCKET_NAME = env.get_string("S3_BUCKET", "cri-intranet")
AWS_S3_ENDPOINT_URL = env.get_string("S3_ENDPOINT")
AWS_S3_SECURE_URLS = env.get_bool("S3_SECURE_URLS", True)
AWS_S3_URL_PROTOCOL = "https:" if env.get_bool("S3_SECURE_URLS", True) else "http:"
# S3 adressing style : auto, virtual or path
AWS_S3_ADDRESSING_STYLE = env.get_bool("S3_ADDRESSING_STYLE", "path")
......@@ -397,14 +400,13 @@ DEBUG_TOOLBAR_PANELS = (
"mail_panel.panels.MailToolbarPanel",
)
# Email settings
DEFAULT_FROM_EMAIL = env.get_string(
"DJANGO_DEFAULT_FROM_EMAIL", f"noreply@{DEFAULT_DOMAIN}"
)
if DEBUG:
if DEBUG and not TESTING:
INSTALLED_APPS += [
"debug_toolbar",
"mail_panel",
......@@ -516,10 +518,6 @@ ALGOLIA = {
if env.get_string("DJANGO_ALGOLIA_INDEX_SUFFIX", ""):
ALGOLIA.update({"INDEX_SUFFIX": env.get_string("DJANGO_ALGOLIA_INDEX_SUFFIX")})
LEGACY_CLASSGROUPS_MAP_FILE = os.path.join(
BASE_DIR, "cri_legacy/data/classgroups_map.csv"
)
# phonenumber_field
PHONENUMBER_DEFAULT_REGION = "FR"
......
......@@ -57,11 +57,10 @@ urlpatterns = [
),
path("cards/", include("cri_cards.urls")),
path("", include("cri_frontend.urls")),
path("", include("cri_legacy.urls")),
path("", include("oidc_provider.urls", namespace="oidc_provider")),
]
if settings.DEBUG:
if settings.DEBUG and not settings.TESTING:
import debug_toolbar
urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns
from django.contrib import admin
from . import models
class LegacyGroupAdmin(admin.ModelAdmin):
list_display = ("name",)
list_display_links = list_display
filter_horizontal = ("members",)
search_fields = ("name",)
@admin.register(models.LegacyClassGroup)
class LegacyClassGroupAdmin(LegacyGroupAdmin):
pass
@admin.register(models.LegacyRole)
class LegacyRoleAdmin(LegacyGroupAdmin):
pass
@admin.register(models.LegacySessionLogEntry)
class LegacySessionLogEntryAdmin(admin.ModelAdmin):
list_display = ("start_at", "end_at", "user", "username", "ip", "image")
list_filter = ("image",)
date_hierarchy = "start_at"
search_fields = (
"user__username",
"user__first_name",
"user__last_name",
"user__email",
"username",
"ip",
"image",
)
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from rest_framework import serializers, generics, permissions
from . import models
import ipaddress
class LegacySessionPingSerializer(serializers.ModelSerializer):
login = serializers.CharField(source="username")
class Meta:
model = models.LegacySessionLogEntry
fields = ("login", "image")
class LegacySessionPing(generics.CreateAPIView):
ALLOWED_SUBNETS = [
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("163.5.0.0/16"),
]
queryset = models.LegacySessionLogEntry.objects.all()
serializer_class = LegacySessionPingSerializer
permission_classes = [permissions.AllowAny]
def perform_create(self, serializer):
try:
user = get_user_model().objects.get(
username=serializer.validated_data.get("username")
)
except get_user_model().DoesNotExist:
user = None
ip = self.request.META.get("REMOTE_ADDR")
if not ip:
raise PermissionDenied
ip = ipaddress.ip_address(ip)
if not any(ip in subnet for subnet in self.ALLOWED_SUBNETS):
raise PermissionDenied
extra_data = {"ip": self.request.META.get("REMOTE_ADDR"), "user": user}
serializer.save(**extra_data)
from django.apps import AppConfig
class CriLegacyConfig(AppConfig):
name = "cri_legacy"
name,promo,slug1,begin_at1,end_at1,slug2,begin_at2,end_at2
2018_app_i3,2018,ing-app-i3-s9-prs,2017-09-01,2018-01-31,ing-app-i3-s10-prs,2018-02-01,2018-08-31
2018_app_x3,2018,ing-app-x3-s9-prs,2017-09-01,2018-01-31,ing-app-x3-s10-prs,2018-02-01,2018-08-31
2018_csi,2018,ing-csi,2017-02-01,2018-01-31,,,
2018_gistre,2018,ing-gistre,2017-02-01,2018-01-31,,,
2018_gitm,2018,ing-gitm,2017-02-01,2018-01-31,,,
2018_inter_me,2018,,,,,,
2018_inter_msc,2018,,,,,,
2018_inter_sum,2018,,,,,,
2018_mti,2018,ing-mti,2017-02-01,2018-01-31,,,
2018_scia,2018,ing-scia,2017-02-01,2018-01-31,,,
2018_sigl,2018,ing-sigl,2017-02-01,2018-01-31,,,
2018_srs,2018,ing-srs,2017-02-01,2018-01-31,,,
2018_tcom,2018,ing-tcom,2017-02-01,2018-01-31,,,
2019_app_i2,2019,ing-app-i2-s7-prs,2017-09-01,2018-01-31,ing-app-i2-s8-prs,2018-02-01,2018-08-31
2019_app_x2,2019,ing-app-x2-s7-prs,2017-09-01,2018-01-31,ing-app-x2-s8-prs,2018-02-01,2018-08-31
2019_appingi_s9,2019,ing-app-i3-s9-prs,2018-09-01,2019-01-31,,,
2019_appingx_s9,2019,ing-app-x3-s9-prs,2018-09-01,2019-01-31,,,
2019_cri,2019,cri,2017-02-01,2019-01-31,,,
2019_gistre,2019,ing-gistre,2018-02-01,2019-01-31,,,
2019_gitm,2019,ing-gitm,2018-02-01,2019-01-31,,,
2019_ing2,2019,ing-ing2-s7-prs,2017-09-01,2018-01-31,ing-ing2-s8-prs,2018-02-01,2018-08-31
2019_inter_me,2019,,,,,,
2019_inter_msc,2019,,,,,,
2019_inter_sum,2019,,,,,,
2019_inter_sum_camp,2019,,,,,,
2019_lrde,2019,labo-lrde,2017-02-01,2019-01-31,,,
2019_lse_sys,2019,labo-lse-sys,2017-02-01,2019-01-31,,,
2019_mti,2019,ing-mti,2018-02-01,2019-01-31,,,
2019_s9,2019,ing-ing3-s9-prs,2018-09-01,2019-01-31,,,
2019_scia,2019,ing-scia,2018-02-01,2019-01-31,,,
2019_sigl,2019,ing-sigl,2018-02-01,2019-01-31,,,
2019_srs,2019,ing-srs,2018-02-01,2019-01-31,,,
2019_tcom,2019,ing-tcom,2018-02-01,2019-01-31,,,
2020_3ie,2020,labo-3ie,2018-02-01,2020-01-31,,,
2020_acu,2020,acu,2019-09-01,2020-01-31,,,
2020_app_i1,2020,ing-app-i1-s5-prs,2017-09-01,2018-01-31,ing-app-i1-s6-prs,2018-02-01,2018-08-31
2020_app_x1,2020,ing-app-x1-s5-prs,2017-09-01,2018-01-31,ing-app-x1-s6-prs,2018-02-01,2018-08-31
2020_appingi_s7,2020,ing-app-i2-s7-prs,2018-09-01,2020-01-31,ing-app-i2-s8-prs,2019-02-01,2019-08-31
2020_appingx_s7,2020,ing-app-x2-s7-prs,2018-09-01,2020-01-31,ing-app-x2-s8-prs,2019-02-01,2019-08-31
2020_cri,2020,cri,2018-02-01,2020-01-31,,,
2020_ing1,2020,ing-ing1-s5-prs,2017-09-01,2018-01-31,ing-ing1-s6-prs,2018-02-01,2018-08-31
2020_inter_me,2020,,,,,,
2020_inter_msc,2020,,,,,,
2020_lrde,2020,labo-lrde,2018-02-01,2020-02-01,,,
2020_lse_ia,2020,labo-lse-ia,2018-02-01,2020-02-01,,,
2020_lse_sys,2020,labo-lse-sys,2018-02-01,2020-02-01,,,
2020_s8_gistre,2020,ing-gistre,2017-02-01,2018-09-01,,,
2020_s8_gitm,2020,ing-gitm,2017-02-01,2018-09-01,,,
2020_s8_image,2020,ing-image,2017-02-01,2018-09-01,,,
2020_s8_mti,2020,ing-mti,2017-02-01,2018-09-01,,,
2020_s8_rdi,2020,ing-rdi,2017-02-01,2018-09-01,,,
2020_s8_scia,2020,ing-scia,2017-02-01,2018-09-01,,,
2020_s8_sigl,2020,ing-sigl,2017-02-01,2018-09-01,,,
2020_s8_srs,2020,ing-srs,2017-02-01,2018-09-01,,,
2020_s8_tcom,2020,ing-tcom,2017-02-01,2018-09-01,,,
2020_seal,2020,labo-seal,2018-02-01,2020-02-01,,,
2020_yaka,2020,yaka,2018-02-01,2019-08-31,,,
2021_3ie,2021,labo-3ie,2019-02-01,2021-02-01,,,
2021_acdc,2021,acdc,2018-09-01,2019-08-31,,,
2021_acu,2021,acu,2018-09-01,2019-01-31,,,
2021_api,2021,prepa-spe-api-s3-prs,2017-09-01,2018-01-31,prepa-spe-api-s4-prs,2018-02-01,2018-08-31
2021_appingi_s5,2021,ing-app-i1-s5-prs,2018-09-01,2019-01-31,ing-app-i1-s6-prs,2019-02-01,2019-08-31
2021_appingx_s5,2021,ing-app-x1-s5-prs,2018-09-01,2019-01-31,ing-app-x1-s6-prs,2019-02-01,2019-08-31
2021_asm,2021,asm,2018-09-01,2019-08-31,,,
2021_cri,2021,cri,2019-02-01,2021-01-31,,,
2021_inter_me,2021,,,,,,
2021_inter_msc_fall,2021,,,,,,
2021_inter_msc_spring,2021,,,,,,
2021_lrde,2021,labo-lrde,2019-02-01,2021-01-31,,,
2021_lse,2021,labo-lse,2019-02-01,2021-01-31,,,
2021_lse_ia,2021,labo-lse-ia,2019-02-01,2021-01-31,,,
2021_lse_sys,2021,labo-lse-sys,2019-02-01,2021-01-31,,,
2021_s3,2021,prepa-spe-s3-prs,2017-09-01,2018-01-31,,,
2021_s4,2021,prepa-spe-s4-prs,2018-02-01,2018-08-31,,,
2021_s4_diese,2021,,,,,,
2021_s4_inter,2021,prepa-spe-s4-inter,2018-02-01,2018-08-31,,,
2021_s5,2021,ing-ing1-s5-prs,2018-09-01,2019-01-31,,,
2021_s6,2021,ing-ing1-s6-prs,2019-02-01,2019-08-31,,,
2021_s8_gistre,2021,ing-gistre,2020-02-01,2021-02-01,,,
2021_s8_gitm,2021,ing-gitm,2020-02-01,2021-02-01,,,
2021_s8_image,2021,ing-image,2020-02-01,2021-02-01,,,
2021_s8_mti,2021,ing-mti,2020-02-01,2021-02-01,,,
2021_s8_scia,2021,ing-scia,2020-02-01,2021-02-01,,,
2021_s8_sigl,2021,ing-sigl,2020-02-01,2021-02-01,,,
2021_s8_srs,2021,ing-srs,2020-02-01,2021-02-01,,,
2021_s8_tcom,2021,ing-tcom,2020-02-01,2021-02-01,,,
2021_seal,2021,labo-seal,2018-02-01,2018-08-31,,,
2021_yaka,2021,yaka,2019-02-01,2020-08-31,,,
2022_acdc,2022,acdc,2019-09-01,2020-08-31,,,
2022_api_s3,2022,prepa-spe-api-s3-prs,2018-09-01,2019-01-31,,,
2022_api_s4,2022,prepa-spe-api-s4-prs,2019-02-01,2019-08-31,,,
2022_appingi_s5,2022,ing-app-i1-s5-prs,2019-09-01,2020-01-31,,,
2022_appingx_s5,2022,ing-app-x1-s5-prs,2019-09-01,2020-01-31,,,
2022_arcs,2022,ing-ing1-arcs-s5-prs,2019-09-01,2020-01-31,ing-ing1-arcs-s6-prs,2020-02-01,2020-08-31
2022_asm,2022,asm,2019-09-01,2020-08-31,,,
2022_cri,2022,cri,2020-02-01,2022-02-01,,,
2022_inter_msc_spring,2022,,,,,,
2022_s1,2022,,,,,,
2022_s1_inter,2022,,,,,,
2022_s2,2022,,,,,,
2022_s2_diese,2022,prepa-sup-s2s-prs,2017-09-01,2018-01-31,,,
2022_s3,2022,,,,,,
2022_s3_diese,2022,prepa-spe-s3s-prs,2018-02-01,2018-08-31,,,
2022_s3_lyon,2022,prepa-spe-s3-lyn,2018-09-01,2019-01-31,,,
2022_s3_paris,2022,prepa-spe-s3-prs,2018-09-01,2019-01-31,,,
2022_s3_rennes,2022,prepa-spe-s3-rns,2018-09-01,2019-01-31,,,
2022_s3_strasbourg,2022,prepa-spe-s3-stg,2018-09-01,2019-01-31,,,
2022_s3_toulouse,2022,prepa-spe-s3-tls,2018-09-01,2019-01-31,,,
2022_s4,2022,prepa-spe-s4-prs,2019-02-01,2019-08-31,,,
2022_s5,2022,ing-ing1-s5-prs,2019-09-01,2020-01-31,,,
2022_s6,2022,ing-ing1-s6-prs,2020-02-01,2020-08-31,,,
2022_seal,2022,labo-seal,2020-02-01,2022-02-01,,,
2022_sup_paris,2022,prepa-sup-s1-prs,2018-09-01,2018-01-31,prepa-sup-s2-prs,2018-02-01,2018-08-31
2022_sup_lyon,2022,prepa-sup-s1-lyn,2018-09-01,2018-01-31,prepa-sup-s2-lyn,2018-02-01,2018-08-31
2022_sup_rennes,2022,prepa-sup-s1-rns,2018-09-01,2018-01-31,prepa-sup-s2-rns,2018-02-01,2018-08-31
2022_sup_strasbourg,2022,prepa-sup-s1-stg,2018-09-01,2018-01-31,prepa-sup-s2-stg,2018-02-01,2018-08-31
2022_sup_toulouse,2022,prepa-sup-s1-tls,2018-09-01,2018-01-31,prepa-sup-s2-tls,2018-02-01,2018-08-31
2023_acdc,2023,acdc,2020-09-01,2021-08-31,,,
2023_appingi_s5,2023,ing-app-i1-s5-prs,2020-09-01,2021-01-31,,,
2023_appingx_s5,2023,ing-app-x1-s5-prs,2020-09-01,2021-01-31,,,
2023_arcs,2023,ing-ing1-arcs-s5-prs,2020-09-01,2021-01-31,,,
2023_asm,2023,asm,2020-09-01,2021-08-31,,,
2023_s1,2023,,,,,,
2023_s1_diese,2023,prepa-sup-s1s-prs,2018-02-01,2019-08-31,,,
2023_s1_lyon,2023,prepa-sup-s1-lyn,2018-09-01,2019-01-31,,,
2023_s1_paris,2023,prepa-sup-s1-prs,2018-09-01,2019-01-31,,,
2023_s1_rennes,2023,prepa-sup-s1-rns,2018-09-01,2019-01-31,,,
2023_s1_strasbourg,2023,prepa-sup-s1-stg,2018-09-01,2019-01-31,,,
2023_s1_toulouse,2023,prepa-sup-s1-tls,2018-09-01,2019-01-31,,,
2023_s2_diese,2023,prepa-sup-s2s-prs,2018-09-01,2020-01-31,,,
2023_s3,2023,,,,,,
2023_s3_lyon,2023,prepa-spe-s3-lyn,2019-09-01,2020-01-31,,,
2023_s3_paris,2023,prepa-spe-s3-prs,2019-09-01,2020-01-31,,,
2023_s3_rennes,2023,prepa-spe-s3-rns,2019-09-01,2020-01-31,,,
2023_s3_strasbourg,2023,prepa-spe-s3-stg,2019-09-01,2020-01-31,,,
2023_s3_toulouse,2023,prepa-spe-s3-tls,2019-09-01,2020-01-31,,,
2023_s4_paris,2023,prepa-spe-s4-prs,2020-02-01,2020-08-31,,,
2023_s5,2023,ing-ing1-s5-prs,2020-09-01,2021-01-31,,,
2024_s1,2024,,,,,,
2024_s1_diese,2024,prepa-sup-s1s-prs,2019-02-01,2019-08-31,,,
2024_s1_lyon,2024,prepa-sup-s1-lyn,2019-09-01,2020-01-31,,,
2024_s1_paris,2024,prepa-sup-s1-prs,2019-09-01,2020-01-31,,,
2024_s1_rennes,2024,prepa-sup-s1-rns,2019-09-01,2020-01-31,,,
2024_s1_strasbourg,2024,prepa-sup-s1-stg,2019-09-01,2020-01-31,,,
2024_s1_toulouse,2024,prepa-sup-s1-tls,2019-09-01,2020-01-31,,,
2024_s2_diese,2024,prepa-sup-s2s-prs,2019-09-01,2020-01-31,,,
2025_s1,2025,,,,,,
2025_s1_diese,2025,prepa-sup-s1s-prs,2020-02-01,2020-08-31,,,
2025_s1_lyon,2025,prepa-sup-s1-lyn,2020-09-01,2021-01-31,,,
2025_s1_paris,2025,prepa-sup-s1-prs,2020-09-01,2021-01-31,,,
2025_s1_rennes,2025,prepa-sup-s1-rns,2020-09-01,2021-01-31,,,
2025_s1_strasbourg,2025,prepa-sup-s1-stg,2020-09-01,2021-01-31,,,
2025_s1_toulouse,2025,prepa-sup-s1-tls,2020-09-01,2021-01-31,,,
3ie,,,,,,,
cri,,,,,,,
inter_exch,,,,,,,
logistique,,,,,,,
lrde,,,,,,,
lse,,,,,,,
lse_ia_permanents,,,,,,,
lse_secu_permanents,,,,,,,
lse_sys_old,,,,,,,
lse_sys_permanents,,,,,,,
profs_fle,,,,,,,
seal_permanents,,,,,,,
securite,,,,,,,
from django.core.management.base import BaseCommand
from django.db.utils import IntegrityError, DataError
from django.conf import settings
from getpass import getpass
from sshpubkeys import SSHKey
from ... import models
from cri_models import models as cri_models
import datetime
import requests
import uuid
import csv
LEGACY_USERS_ENDPOINT = "https://cri.epita.fr/api/users"
SSH_KEYS_ENDPOINT = "https://cri.epita.fr/api/users/{login}/ssh_keys"
def parse_date(value):
return datetime.date.fromisoformat(value)
class Command(BaseCommand):
help = "Import legacy users"
def load_cg_map(self):
self.CG_MAP = {}
with open(settings.LEGACY_CLASSGROUPS_MAP_FILE, "r") as f:
cr = csv.DictReader(f)
for data in cr:
if "name" not in data or "promo" not in data:
raise ValueError("Invalid CSV")
self.CG_MAP.setdefault(data["name"], [])
for slug_field in [k for k in data.keys() if k.startswith("slug")]:
begin_at_field = "begin_at" + slug_field.replace("slug", "")
end_at_field = "end_at" + slug_field.replace("slug", "")
if begin_at_field not in data or end_at_field not in data:
raise ValueError("Invalid CSV")
if data[slug_field]:
self.CG_MAP[data["name"]].append(
(
cri_models.CRIGroup.objects.get(slug=data[slug_field]),
data["promo"],
parse_date(data[begin_at_field]),
parse_date(data[end_at_field]),
)
)
def handle(self, *args, **options):
self.load_cg_map()
with requests.Session() as session:
login = input("login: ")
password = getpass("password: ")
session.auth = (login, password)
next_url = LEGACY_USERS_ENDPOINT
max_count = "?"
count = 0
while next_url:
r = session.get(next_url)
if r.status_code != 200:
raise RuntimeError(repr(r))
data = r.json()
next_url = data.get("next")
max_count = data.get("count", "?")
for user_data in data.get("results", []):
count += 1
try:
self.import_user(user_data, session)
except Exception as e:
print(f"ERROR: {user_data}: {e}")
print(f"Importing... {count}/{max_count}")
for group in cri_models.CRIGroup.objects.all():
group.update_computed_entries()
def import_user(self, user_data, session):
login = user_data.get("login")
if not login:
raise ValueError("missing login")
GID_MAP = {
5000: "wheel",
6001: "users",
7000: "administratives",
10016: "students",
10017: "students",
10018: "students",
10019: "students",
10020: "students",
10021: "students",
10022: "students",
10023: "students",
10024: "students",
10025: "students",
9000: "users",
8000: "users",
6000: "teachers",
8001: "users",
}
FIELD_MAP = {
"first_name": "firstname",
"last_name": "lastname",
"legal_first_name": "firstname",
"legal_last_name": "lastname",
"phone": "telephone",
"email": "mail",
"uid": "uidNumber",
"birthdate": "birthdate",
}
values = {
field: user_data.get(legacy_field)
for field, legacy_field in FIELD_MAP.items()
if user_data.get(legacy_field) is not None
}
values["primary_group"] = cri_models.CRIGroup.objects.get(
slug=GID_MAP.get(user_data.get("gidNumber"), "users")
)
user, _created = cri_models.CRIUser.objects.update_or_create(
username=login, defaults=values
)
primary = user.username.replace(".", "_")
(principal, _created,) = cri_models.KerberosPrincipal.objects.get_or_create(
user=user,
principal=f"{primary}@CRI.EPITA.NET",
defaults={"out_of_date": False},
)
if not user.primary_principal:
user.primary_principal = principal
user.save()
for cg_name in user_data.get("class_groups", []):
cg, _created = models.LegacyClassGroup.objects.get_or_create(name=cg_name)
cg.members.add(user)
self.add_to_group(user, cg_name)
self.import_ssh_keys(user, session)
def add_to_group(self, user, cg_name):
if cg_name not in self.CG_MAP:
raise ValueError(f"unknown class group: {cg_name}")
memberships = []
for group, graduation_year, begin_at, end_at in self.CG_MAP[cg_name]:
memberships.append(
cri_models.CRIMembership(
user=user,
group=group,
graduation_year=graduation_year,
begin_at=begin_at,
end_at=end_at,
)
)
cri_models.CRIMembership.objects.bulk_create(memberships)
def import_ssh_keys(self, user, session):
r = session.get(SSH_KEYS_ENDPOINT.format(login=user.username))
if r.status_code != 200:
raise RuntimeError(repr(r))
for key in map(SSHKey, r.json().get("keys", [])):
try:
cri_models.SSHPublicKey.objects.update_or_create(
key=key,
defaults={
"user": user,
"key_type": key.key_type,
"key_length": key.bits,
},
)
except DataError:
title = f"title truncated: {key.comment}"[:128]
cri_models.SSHPublicKey.objects.update_or_create(
key=key,
defaults={
"user": user,
"key_type": key.key_type,
"key_length": key.bits,
"title": title,
},
)
except IntegrityError:
title = f"duplicate title {uuid.uuid4()}: {key.comment}"[:128]
cri_models.SSHPublicKey.objects.update_or_create(
key=key,
defaults={
"user": user,
"key_type": key.key_type,
"key_length": key.bits,
"title": title,
},
)