Skip to content

Commit

Permalink
core: add attributes. avatar method to allow custom uploaded avatars
Browse files Browse the repository at this point in the history
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#2631
  • Loading branch information
BeryJu committed Jul 26, 2022
1 parent 55739ee commit de26c65
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 17 deletions.
6 changes: 4 additions & 2 deletions authentik/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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
from authentik.lib.config import CONFIG, get_path_from_dict
from authentik.lib.generators import generate_id
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
from authentik.lib.utils.http import get_client_ip
Expand Down Expand Up @@ -213,9 +213,11 @@ def avatar(self) -> str:
mode: str = CONFIG.y("avatars", "none")
if mode == "none":
return DEFAULT_AVATAR
# gravatar uses md5 for their URLs, so md5 can't be avoided
if mode.startswith("attributes."):
return get_path_from_dict(self.attributes, mode[11:], default=DEFAULT_AVATAR)
mail_hash = md5(self.email.lower().encode("utf-8")).hexdigest() # nosec
if mode == "gravatar":
# gravatar uses md5 for their URLs, so md5 can't be avoided
parameters = [
("s", "158"),
("r", "g"),
Expand Down
47 changes: 47 additions & 0 deletions authentik/core/tests/test_users_api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Test Users API"""
from json import loads

from django.urls.base import reverse
from rest_framework.test import APITestCase

from authentik.core.models import 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
Expand Down Expand Up @@ -211,3 +214,47 @@ def test_path_invalid(self):
self.assertJSONEqual(
response.content.decode(), {"path": ["No empty segments in user path allowed."]}
)

def test_me(self):
"""Test user's me endpoint"""
self.client.force_login(self.admin)
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")
18 changes: 12 additions & 6 deletions authentik/lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")


def get_path_from_dict(root: dict, path: str, sep=".", default=None):
"""Recursively walk through `root`, checking each part of `path` split by `sep`.
If at any point a dict does not exist, return default"""
for comp in path.split(sep):
if root and comp in root:
root = root.get(comp)
else:
return default
return root


class ConfigLoader:
"""Search through SEARCH_PATHS and load configuration. Environment variables starting with
`ENV_PREFIX` are also applied.
Expand Down Expand Up @@ -155,12 +166,7 @@ def y(self, path: str, default=None, sep=".") -> Any:
# Walk sub_dicts before parsing path
root = self.raw
# Walk each component of the path
for comp in path.split(sep):
if root and comp in root:
root = root.get(comp)
else:
return default
return root
return get_path_from_dict(root, path, sep=sep, default=default)

def y_set(self, path: str, value: Any, sep="."):
"""Set value using same syntax as y()"""
Expand Down
11 changes: 2 additions & 9 deletions authentik/stages/prompt/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""prompt models"""
from base64 import b64decode
from binascii import Error
from typing import Any, Optional
from urllib.parse import urlparse
from uuid import uuid4
Expand Down Expand Up @@ -87,16 +85,11 @@ def to_internal_value(self, data: str):
uri = urlparse(data)
if uri.scheme != "data":
raise ValidationError("Invalid scheme")
header, encoded = uri.path.split(",", 1)
header, _encoded = uri.path.split(",", 1)
_mime, _, enc = header.partition(";")
if enc != "base64":
raise ValidationError("Invalid encoding")
try:
data = b64decode(encoded.encode()).decode()
except (UnicodeDecodeError, UnicodeEncodeError, ValueError, Error):
LOGGER.info("failed to decode base64 of file field, keeping base64")
data = encoded
return super().to_internal_value(data)
return super().to_internal_value(uri)


class Prompt(SerializerModel):
Expand Down
3 changes: 3 additions & 0 deletions website/docs/installation/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ Configure how authentik should show avatars for users. Following values can be s
- `%(mail_hash)s`: The email address, md5 hashed
- `%(upn)s`: The user's UPN, if set (otherwise an empty string)

Starting with authentik 2022.8, you can also use an attribute path like `attributes.something.avatar`,
which can be used in combination with the file field to allow users to upload custom avatars for themselves.

### `AUTHENTIK_DEFAULT_USER_CHANGE_NAME`

:::info
Expand Down

0 comments on commit de26c65

Please sign in to comment.