Compare commits
4 Commits
admin-layo
...
root/more-
Author | SHA1 | Date | |
---|---|---|---|
06337283e8 | |||
5d28114a4b | |||
b7c154ccd2 | |||
d1fbf2ed65 |
@ -209,7 +209,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="superuser_full_list",
|
name="list_rbac",
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
type=OpenApiTypes.BOOL,
|
type=OpenApiTypes.BOOL,
|
||||||
),
|
),
|
||||||
@ -229,10 +229,8 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Custom list method that checks Policy based access instead of guardian"""
|
"""Custom list method that checks Policy based access instead of guardian"""
|
||||||
should_cache = request.query_params.get("search", "") == ""
|
should_cache = request.query_params.get("search", "") == ""
|
||||||
|
|
||||||
superuser_full_list = (
|
list_rbac = str(request.query_params.get("list_rbac", "false")).lower() == "true"
|
||||||
str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
|
if list_rbac:
|
||||||
)
|
|
||||||
if superuser_full_list and request.user.is_superuser:
|
|
||||||
return super().list(request)
|
return super().list(request)
|
||||||
|
|
||||||
only_with_launch_url = str(
|
only_with_launch_url = str(
|
||||||
|
@ -4,7 +4,7 @@ from typing import Any
|
|||||||
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||||
from guardian.shortcuts import assign_perm, get_anonymous_user
|
from guardian.shortcuts import assign_perm
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
@ -138,13 +138,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
owner_field = "user"
|
owner_field = "user"
|
||||||
rbac_allow_create_without_perm = True
|
rbac_allow_create_without_perm = True
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
|
||||||
return super().get_queryset().filter(user=user.pk)
|
|
||||||
|
|
||||||
def perform_create(self, serializer: TokenSerializer):
|
def perform_create(self, serializer: TokenSerializer):
|
||||||
|
# TODO: better permission check
|
||||||
if not self.request.user.is_superuser:
|
if not self.request.user.is_superuser:
|
||||||
instance = serializer.save(
|
instance = serializer.save(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
57
authentik/core/migrations/0043_alter_user_options.py
Normal file
57
authentik/core/migrations/0043_alter_user_options.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2025-01-08 17:39
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_user_debug_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
from django.apps import apps as real_apps
|
||||||
|
from django.contrib.auth.management import create_permissions
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
User = apps.get_model("authentik_core", "User")
|
||||||
|
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||||
|
|
||||||
|
# Permissions are only created _after_ migrations are run
|
||||||
|
# - https://github.com/django/django/blob/43cdfa8b20e567a801b7d0a09ec67ddd062d5ea4/django/contrib/auth/apps.py#L19
|
||||||
|
# - https://stackoverflow.com/a/72029063/1870445
|
||||||
|
create_permissions(real_apps.get_app_config("authentik_core"), using=db_alias)
|
||||||
|
|
||||||
|
Permission = apps.get_model("auth", "Permission")
|
||||||
|
|
||||||
|
new_prem = Permission.objects.using(db_alias).get(codename="user_view_debug")
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for user in User.objects.using(db_alias).filter(
|
||||||
|
**{f"attributes__{USER_ATTRIBUTE_DEBUG}": True}
|
||||||
|
):
|
||||||
|
user.permissions.add(new_prem)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="user",
|
||||||
|
options={
|
||||||
|
"permissions": [
|
||||||
|
("reset_user_password", "Reset Password"),
|
||||||
|
("impersonate", "Can impersonate other users"),
|
||||||
|
("assign_user_permissions", "Can assign permissions to users"),
|
||||||
|
("unassign_user_permissions", "Can unassign permissions from users"),
|
||||||
|
("preview_user", "Can preview user data sent to providers"),
|
||||||
|
("view_user_applications", "View applications the user has access to"),
|
||||||
|
("user_view_debug", "User receives additional details for error messages"),
|
||||||
|
],
|
||||||
|
"verbose_name": "User",
|
||||||
|
"verbose_name_plural": "Users",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_user_debug_attribute),
|
||||||
|
]
|
@ -41,7 +41,6 @@ from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGT
|
|||||||
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
|
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
|
||||||
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
|
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
|
||||||
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
||||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
||||||
@ -282,6 +281,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
|||||||
("unassign_user_permissions", _("Can unassign permissions from users")),
|
("unassign_user_permissions", _("Can unassign permissions from users")),
|
||||||
("preview_user", _("Can preview user data sent to providers")),
|
("preview_user", _("Can preview user data sent to providers")),
|
||||||
("view_user_applications", _("View applications the user has access to")),
|
("view_user_applications", _("View applications the user has access to")),
|
||||||
|
("user_view_debug", _("User receives additional details for error messages")),
|
||||||
]
|
]
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["last_login"]),
|
models.Index(fields=["last_login"]),
|
||||||
|
@ -96,7 +96,7 @@ class EndpointViewSet(UsedByMixin, ModelViewSet):
|
|||||||
OpenApiTypes.STR,
|
OpenApiTypes.STR,
|
||||||
),
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="superuser_full_list",
|
name="list_rbac",
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
type=OpenApiTypes.BOOL,
|
type=OpenApiTypes.BOOL,
|
||||||
),
|
),
|
||||||
@ -110,8 +110,8 @@ class EndpointViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""List accessible endpoints"""
|
"""List accessible endpoints"""
|
||||||
should_cache = request.GET.get("search", "") == ""
|
should_cache = request.GET.get("search", "") == ""
|
||||||
|
|
||||||
superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true"
|
list_rbac = str(request.GET.get("list_rbac", "false")).lower() == "true"
|
||||||
if superuser_full_list and request.user.is_superuser:
|
if list_rbac:
|
||||||
return super().list(request)
|
return super().list(request)
|
||||||
|
|
||||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
|
@ -97,12 +97,9 @@ class FlowErrorChallenge(Challenge):
|
|||||||
if not request or not error:
|
if not request or not error:
|
||||||
return
|
return
|
||||||
self.initial_data["request_id"] = request.request_id
|
self.initial_data["request_id"] = request.request_id
|
||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
|
||||||
|
|
||||||
if request.user and request.user.is_authenticated:
|
if request.user and request.user.is_authenticated:
|
||||||
if request.user.is_superuser or request.user.group_attributes(request).get(
|
if request.user.has_perm("authentik_core.user_view_debug"):
|
||||||
USER_ATTRIBUTE_DEBUG, False
|
|
||||||
):
|
|
||||||
self.initial_data["error"] = str(error)
|
self.initial_data["error"] = str(error)
|
||||||
self.initial_data["traceback"] = exception_to_string(error)
|
self.initial_data["traceback"] = exception_to_string(error)
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from paramiko.ssh_exception import SSHException
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
|
from authentik import __version__
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||||
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
||||||
@ -182,10 +183,16 @@ class DockerController(BaseController):
|
|||||||
`outposts.container_image_base`, but fall back to known-good images"""
|
`outposts.container_image_base`, but fall back to known-good images"""
|
||||||
image = self.get_container_image()
|
image = self.get_container_image()
|
||||||
try:
|
try:
|
||||||
self.client.images.pull(image)
|
# See if the image exists...
|
||||||
except DockerException: # pragma: no cover
|
self.client.images.get(image)
|
||||||
image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
|
except DockerException:
|
||||||
self.client.images.pull(image)
|
try:
|
||||||
|
# ...otherwise try to pull it...
|
||||||
|
self.client.images.pull(image)
|
||||||
|
except DockerException:
|
||||||
|
# ...and as a fallback to that default to a sane standard
|
||||||
|
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
|
||||||
|
self.client.images.pull(image)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def _get_container(self) -> tuple[Container, bool]:
|
def _get_container(self) -> tuple[Container, bool]:
|
||||||
|
@ -7,7 +7,7 @@ from django.template.response import TemplateResponse
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
from authentik.core.models import User
|
||||||
from authentik.policies.types import PolicyResult
|
from authentik.policies.types import PolicyResult
|
||||||
|
|
||||||
|
|
||||||
@ -31,12 +31,11 @@ class AccessDeniedResponse(TemplateResponse):
|
|||||||
if self.error_message:
|
if self.error_message:
|
||||||
context["error"] = self.error_message
|
context["error"] = self.error_message
|
||||||
# Only show policy result if user is authenticated and
|
# Only show policy result if user is authenticated and
|
||||||
# either superuser or has USER_ATTRIBUTE_DEBUG set
|
# has permissions to see them
|
||||||
if self.policy_result:
|
if self.policy_result:
|
||||||
if self._request.user and self._request.user.is_authenticated:
|
if self._request.user and self._request.user.is_authenticated:
|
||||||
if self._request.user.is_superuser or self._request.user.group_attributes(
|
user: User = self._request.user
|
||||||
self._request
|
if user.has_perm("authentik_core.user_view_debug"):
|
||||||
).get(USER_ATTRIBUTE_DEBUG, False):
|
|
||||||
context["policy_result"] = self.policy_result
|
context["policy_result"] = self.policy_result
|
||||||
context["cancel"] = reverse("authentik_flows:cancel")
|
context["cancel"] = reverse("authentik_flows:cancel")
|
||||||
return context
|
return context
|
||||||
|
@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from guardian.utils import get_anonymous_user
|
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
@ -66,17 +63,7 @@ class AuthorizationCodeViewSet(
|
|||||||
serializer_class = ExpiringBaseGrantModelSerializer
|
serializer_class = ExpiringBaseGrantModelSerializer
|
||||||
filterset_fields = ["user", "provider"]
|
filterset_fields = ["user", "provider"]
|
||||||
ordering = ["provider", "expires"]
|
ordering = ["provider", "expires"]
|
||||||
filter_backends = [
|
owner_field = "user"
|
||||||
DjangoFilterBackend,
|
|
||||||
OrderingFilter,
|
|
||||||
SearchFilter,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
|
||||||
return super().get_queryset().filter(user=user.pk)
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenViewSet(
|
class RefreshTokenViewSet(
|
||||||
@ -92,17 +79,7 @@ class RefreshTokenViewSet(
|
|||||||
serializer_class = TokenModelSerializer
|
serializer_class = TokenModelSerializer
|
||||||
filterset_fields = ["user", "provider"]
|
filterset_fields = ["user", "provider"]
|
||||||
ordering = ["provider", "expires"]
|
ordering = ["provider", "expires"]
|
||||||
filter_backends = [
|
owner_field = "user"
|
||||||
DjangoFilterBackend,
|
|
||||||
OrderingFilter,
|
|
||||||
SearchFilter,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
|
||||||
return super().get_queryset().filter(user=user.pk)
|
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenViewSet(
|
class AccessTokenViewSet(
|
||||||
@ -118,14 +95,4 @@ class AccessTokenViewSet(
|
|||||||
serializer_class = TokenModelSerializer
|
serializer_class = TokenModelSerializer
|
||||||
filterset_fields = ["user", "provider"]
|
filterset_fields = ["user", "provider"]
|
||||||
ordering = ["provider", "expires"]
|
ordering = ["provider", "expires"]
|
||||||
filter_backends = [
|
owner_field = "user"
|
||||||
DjangoFilterBackend,
|
|
||||||
OrderingFilter,
|
|
||||||
SearchFilter,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
|
||||||
return super().get_queryset().filter(user=user.pk)
|
|
||||||
|
@ -6445,6 +6445,7 @@
|
|||||||
"authentik_core.remove_user_from_group",
|
"authentik_core.remove_user_from_group",
|
||||||
"authentik_core.reset_user_password",
|
"authentik_core.reset_user_password",
|
||||||
"authentik_core.unassign_user_permissions",
|
"authentik_core.unassign_user_permissions",
|
||||||
|
"authentik_core.user_view_debug",
|
||||||
"authentik_core.view_application",
|
"authentik_core.view_application",
|
||||||
"authentik_core.view_applicationentitlement",
|
"authentik_core.view_applicationentitlement",
|
||||||
"authentik_core.view_authenticatedsession",
|
"authentik_core.view_authenticatedsession",
|
||||||
@ -12694,6 +12695,7 @@
|
|||||||
"authentik_core.remove_user_from_group",
|
"authentik_core.remove_user_from_group",
|
||||||
"authentik_core.reset_user_password",
|
"authentik_core.reset_user_password",
|
||||||
"authentik_core.unassign_user_permissions",
|
"authentik_core.unassign_user_permissions",
|
||||||
|
"authentik_core.user_view_debug",
|
||||||
"authentik_core.view_application",
|
"authentik_core.view_application",
|
||||||
"authentik_core.view_applicationentitlement",
|
"authentik_core.view_applicationentitlement",
|
||||||
"authentik_core.view_authenticatedsession",
|
"authentik_core.view_authenticatedsession",
|
||||||
@ -13202,6 +13204,7 @@
|
|||||||
"unassign_user_permissions",
|
"unassign_user_permissions",
|
||||||
"preview_user",
|
"preview_user",
|
||||||
"view_user_applications",
|
"view_user_applications",
|
||||||
|
"user_view_debug",
|
||||||
"add_user",
|
"add_user",
|
||||||
"change_user",
|
"change_user",
|
||||||
"delete_user",
|
"delete_user",
|
||||||
|
@ -4,6 +4,9 @@ version = "2024.12.2"
|
|||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
|
[tool.poetry.requires-plugins]
|
||||||
|
poetry-plugin-export = ">1.8"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = ['py312']
|
target-version = ['py312']
|
||||||
|
16
schema.yml
16
schema.yml
@ -3391,6 +3391,10 @@ paths:
|
|||||||
name: group
|
name: group
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: list_rbac
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
- in: query
|
- in: query
|
||||||
name: meta_description
|
name: meta_description
|
||||||
schema:
|
schema:
|
||||||
@ -3439,10 +3443,6 @@ paths:
|
|||||||
name: slug
|
name: slug
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- in: query
|
|
||||||
name: superuser_full_list
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
tags:
|
tags:
|
||||||
- core
|
- core
|
||||||
security:
|
security:
|
||||||
@ -23204,6 +23204,10 @@ paths:
|
|||||||
operationId: rac_endpoints_list
|
operationId: rac_endpoints_list
|
||||||
description: List accessible endpoints
|
description: List accessible endpoints
|
||||||
parameters:
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: list_rbac
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
- in: query
|
- in: query
|
||||||
name: name
|
name: name
|
||||||
schema:
|
schema:
|
||||||
@ -23234,10 +23238,6 @@ paths:
|
|||||||
name: search
|
name: search
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- in: query
|
|
||||||
name: superuser_full_list
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
tags:
|
tags:
|
||||||
- rac
|
- rac
|
||||||
security:
|
security:
|
||||||
|
@ -66,7 +66,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
|||||||
async apiEndpoint(): Promise<PaginatedResponse<Application>> {
|
async apiEndpoint(): Promise<PaginatedResponse<Application>> {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
|
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
|
||||||
...(await this.defaultEndpointConfig()),
|
...(await this.defaultEndpointConfig()),
|
||||||
superuserFullList: true,
|
listRbac: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ export class BrandForm extends ModelForm<Brand, string> {
|
|||||||
.fetchObjects=${async (query?: string): Promise<Application[]> => {
|
.fetchObjects=${async (query?: string): Promise<Application[]> => {
|
||||||
const args: CoreApplicationsListRequest = {
|
const args: CoreApplicationsListRequest = {
|
||||||
ordering: "name",
|
ordering: "name",
|
||||||
superuserFullList: true,
|
listRbac: true,
|
||||||
};
|
};
|
||||||
if (query !== undefined) {
|
if (query !== undefined) {
|
||||||
args.search = query;
|
args.search = query;
|
||||||
|
@ -46,7 +46,7 @@ export class EndpointListPage extends Table<Endpoint> {
|
|||||||
return new RacApi(DEFAULT_CONFIG).racEndpointsList({
|
return new RacApi(DEFAULT_CONFIG).racEndpointsList({
|
||||||
...(await this.defaultEndpointConfig()),
|
...(await this.defaultEndpointConfig()),
|
||||||
provider: this.provider?.pk,
|
provider: this.provider?.pk,
|
||||||
superuserFullList: true,
|
listRbac: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user