core: Add support for auto generating unique avatars based on the user's initials (#4663)

This commit is contained in:
sdimovv
2023-02-12 15:35:17 +00:00
committed by GitHub
parent 21e29744c2
commit b69e55eae9
7 changed files with 285 additions and 72 deletions

View File

@ -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 = (

View File

@ -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()

View 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"])