core: Add support for auto generating unique avatars based on the user's initials (#4663)
This commit is contained in:
		| @ -1,8 +1,7 @@ | ||||
| """authentik core models""" | ||||
| from datetime import timedelta | ||||
| from hashlib import md5, sha256 | ||||
| from hashlib import sha256 | ||||
| from typing import Any, Optional | ||||
| from urllib.parse import urlencode | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from deepmerge import always_merger | ||||
| @ -13,9 +12,7 @@ from django.contrib.auth.models import UserManager as DjangoUserManager | ||||
| from django.db import models | ||||
| from django.db.models import Q, QuerySet, options | ||||
| from django.http import HttpRequest | ||||
| from django.templatetags.static import static | ||||
| from django.utils.functional import SimpleLazyObject, cached_property | ||||
| from django.utils.html import escape | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from guardian.mixins import GuardianUserMixin | ||||
| @ -27,7 +24,8 @@ from authentik.blueprints.models import ManagedModel | ||||
| from authentik.core.exceptions import PropertyMappingExpressionException | ||||
| from authentik.core.signals import password_changed | ||||
| from authentik.core.types import UILoginButton, UserSettingSerializer | ||||
| from authentik.lib.config import CONFIG, get_path_from_dict | ||||
| from authentik.lib.avatars import get_avatar | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel | ||||
| from authentik.lib.utils.http import get_client_ip | ||||
| @ -49,9 +47,6 @@ USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" | ||||
| USER_PATH_SYSTEM_PREFIX = "goauthentik.io" | ||||
| USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts" | ||||
|  | ||||
| GRAVATAR_URL = "https://secure.gravatar.com" | ||||
| DEFAULT_AVATAR = static("dist/assets/images/user_default.png") | ||||
|  | ||||
|  | ||||
| options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",) | ||||
|  | ||||
| @ -233,25 +228,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser): | ||||
|     @property | ||||
|     def avatar(self) -> str: | ||||
|         """Get avatar, depending on authentik.avatar setting""" | ||||
|         mode: str = CONFIG.y("avatars", "none") | ||||
|         if mode == "none": | ||||
|             return DEFAULT_AVATAR | ||||
|         if mode.startswith("attributes."): | ||||
|             return get_path_from_dict(self.attributes, mode[11:], default=DEFAULT_AVATAR) | ||||
|         # gravatar uses md5 for their URLs, so md5 can't be avoided | ||||
|         mail_hash = md5(self.email.lower().encode("utf-8")).hexdigest()  # nosec | ||||
|         if mode == "gravatar": | ||||
|             parameters = [ | ||||
|                 ("s", "158"), | ||||
|                 ("r", "g"), | ||||
|             ] | ||||
|             gravatar_url = f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}" | ||||
|             return escape(gravatar_url) | ||||
|         return mode % { | ||||
|             "username": self.username, | ||||
|             "mail_hash": mail_hash, | ||||
|             "upn": self.attributes.get("upn", ""), | ||||
|         } | ||||
|         return get_avatar(self) | ||||
|  | ||||
|     class Meta: | ||||
|         permissions = ( | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| """Test Users API""" | ||||
| from json import loads | ||||
|  | ||||
| from django.contrib.sessions.backends.cache import KEY_PREFIX | ||||
| from django.core.cache import cache | ||||
| @ -9,7 +8,6 @@ from rest_framework.test import APITestCase | ||||
| from authentik.core.models import AuthenticatedSession, User | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant | ||||
| from authentik.flows.models import FlowDesignation | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.generators import generate_id, generate_key | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.tenants.models import Tenant | ||||
| @ -222,44 +220,6 @@ class TestUsersAPI(APITestCase): | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     @CONFIG.patch("avatars", "none") | ||||
|     def test_avatars_none(self): | ||||
|         """Test avatars none""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["user"]["avatar"], "/static/dist/assets/images/user_default.png") | ||||
|  | ||||
|     @CONFIG.patch("avatars", "gravatar") | ||||
|     def test_avatars_gravatar(self): | ||||
|         """Test avatars gravatar""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertIn("gravatar", body["user"]["avatar"]) | ||||
|  | ||||
|     @CONFIG.patch("avatars", "foo-%(username)s") | ||||
|     def test_avatars_custom(self): | ||||
|         """Test avatars custom""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["user"]["avatar"], f"foo-{self.admin.username}") | ||||
|  | ||||
|     @CONFIG.patch("avatars", "attributes.foo.avatar") | ||||
|     def test_avatars_attributes(self): | ||||
|         """Test avatars attributes""" | ||||
|         self.admin.attributes = {"foo": {"avatar": "bar"}} | ||||
|         self.admin.save() | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["user"]["avatar"], "bar") | ||||
|  | ||||
|     def test_session_delete(self): | ||||
|         """Ensure sessions are deleted when a user is deactivated""" | ||||
|         user = create_test_admin_user() | ||||
|  | ||||
							
								
								
									
										84
									
								
								authentik/core/tests/test_users_avatars.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								authentik/core/tests/test_users_avatars.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| """Test Users Avatars""" | ||||
| from json import loads | ||||
|  | ||||
| from django.urls.base import reverse | ||||
| from requests_mock import Mocker | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.lib.config import CONFIG | ||||
|  | ||||
|  | ||||
| class TestUsersAvatars(APITestCase): | ||||
|     """Test Users avatars""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.admin = create_test_admin_user() | ||||
|         self.user = User.objects.create(username="test-user") | ||||
|  | ||||
|     @CONFIG.patch("avatars", "none") | ||||
|     def test_avatars_none(self): | ||||
|         """Test avatars none""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["user"]["avatar"], "/static/dist/assets/images/user_default.png") | ||||
|  | ||||
|     @CONFIG.patch("avatars", "gravatar") | ||||
|     def test_avatars_gravatar(self): | ||||
|         """Test avatars gravatar""" | ||||
|         self.admin.email = "static@t.goauthentik.io" | ||||
|         self.admin.save() | ||||
|         self.client.force_login(self.admin) | ||||
|         with Mocker() as mocker: | ||||
|             mocker.head( | ||||
|                 ( | ||||
|                     "https://secure.gravatar.com/avatar/84730f9c1851d1ea03f1a" | ||||
|                     "a9ed85bd1ea?size=158&rating=g&default=404" | ||||
|                 ), | ||||
|                 text="foo", | ||||
|             ) | ||||
|             response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertIn("gravatar", body["user"]["avatar"]) | ||||
|  | ||||
|     @CONFIG.patch("avatars", "initials") | ||||
|     def test_avatars_initials(self): | ||||
|         """Test avatars initials""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertIn("data:image/svg+xml;base64,", body["user"]["avatar"]) | ||||
|  | ||||
|     @CONFIG.patch("avatars", "foo://%(username)s") | ||||
|     def test_avatars_custom(self): | ||||
|         """Test avatars custom""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["user"]["avatar"], f"foo://{self.admin.username}") | ||||
|  | ||||
|     @CONFIG.patch("avatars", "attributes.foo.avatar") | ||||
|     def test_avatars_attributes(self): | ||||
|         """Test avatars attributes""" | ||||
|         self.admin.attributes = {"foo": {"avatar": "bar"}} | ||||
|         self.admin.save() | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["user"]["avatar"], "bar") | ||||
|  | ||||
|     @CONFIG.patch("avatars", "attributes.foo.avatar,initials") | ||||
|     def test_avatars_fallback(self): | ||||
|         """Test fallback""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.get(reverse("authentik_api:user-me")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertIn("data:image/svg+xml;base64,", body["user"]["avatar"]) | ||||
		Reference in New Issue
	
	Block a user
	 sdimovv
					sdimovv