mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
sources: add Telegram source (#15749)
* sources: add Telegram source (#2232) * sources/telegram: put telegram user info into policy context (#2232) * sources/telegram: replace regular input for bot token with a "secret" one (#2232) * sources/telegram: fix typo on Telegram source form * sources/telegram: added UserSourceConnection/GroupSourceConnection and SourceFlowManager subclasses for Telegram source * sources/telegram: improved code layout * sources/telegram: collapsed migrations * sources/telegram: fix lint errors * sources/telegram: fixed lint errors in docs * sources/telegram: fix app config * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * Update website/docs/users-sources/sources/social-logins/telegram/index.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> * sources/telegram: add user source settings UI so that the users can disconnect Telegram source from their account * sources/telegram: clean up code per @risson's suggestions * sources/telegram: improve docs based on @tanberry's suggestions * sources/telegram: fix minor docs formatting issue * sources/teleram: add tests for views * sources/telegram: update serielizer field types references to be in line with convention * sources/telegram: add missing type annotations * sources/telegram: add check for source.enabled in the redirect view * sources/telegram: add pre-authentication flow to telegram source * sources: add Telegram source (#2232) * sources/telegram: added UserSourceConnection/GroupSourceConnection and SourceFlowManager subclasses for Telegram source * sources/telegram: collapsed migrations * sources/telegram: fix lint errors * sources/telegram: clean up code per @risson's suggestions * sources/teregram: fix merge errors * sources/telegram: improve docs wording * Standardized documentation * sources/telegram: added telegram source package to the list of ignored modules for mypy * sources/telegram: fix TS lint errors * sources/telegram: improve test coverage * web: bump @types/node from 22.15.19 to 24.5.2 in /web (#16989) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.19 to 24.5.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 24.5.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Co-authored-by: dewi-tik <dewi@goauthentik.io> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1f2d411a7c
commit
eeb5cb08cd
@@ -103,6 +103,7 @@ TENANT_APPS = [
|
||||
"authentik.sources.plex",
|
||||
"authentik.sources.saml",
|
||||
"authentik.sources.scim",
|
||||
"authentik.sources.telegram",
|
||||
"authentik.stages.authenticator",
|
||||
"authentik.stages.authenticator_duo",
|
||||
"authentik.stages.authenticator_email",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Telegram source property mappings API"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.telegram.models import TelegramSourcePropertyMapping
|
||||
|
||||
|
||||
class TelegramSourcePropertyMappingSerializer(PropertyMappingSerializer):
|
||||
"""TelegramSourcePropertyMapping Serializer"""
|
||||
|
||||
class Meta(PropertyMappingSerializer.Meta):
|
||||
model = TelegramSourcePropertyMapping
|
||||
|
||||
|
||||
class TelegramSourcePropertyMappingFilter(PropertyMappingFilterSet):
|
||||
"""Filter for TelegramSourcePropertyMapping"""
|
||||
|
||||
class Meta(PropertyMappingFilterSet.Meta):
|
||||
model = TelegramSourcePropertyMapping
|
||||
|
||||
|
||||
class TelegramSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""TelegramSourcePropertyMapping Viewset"""
|
||||
|
||||
queryset = TelegramSourcePropertyMapping.objects.all()
|
||||
serializer_class = TelegramSourcePropertyMappingSerializer
|
||||
filterset_class = TelegramSourcePropertyMappingFilter
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
@@ -0,0 +1,41 @@
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.telegram.models import TelegramSource
|
||||
|
||||
|
||||
class TelegramSourceSerializer(SourceSerializer):
|
||||
class Meta:
|
||||
model = TelegramSource
|
||||
fields = SourceSerializer.Meta.fields + [
|
||||
"bot_username",
|
||||
"bot_token",
|
||||
"request_message_access",
|
||||
"pre_authentication_flow",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"bot_token": {"write_only": True},
|
||||
}
|
||||
|
||||
|
||||
class TelegramSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = TelegramSource.objects.all()
|
||||
serializer_class = TelegramSourceSerializer
|
||||
lookup_field = "slug"
|
||||
|
||||
filterset_fields = [
|
||||
"pbm_uuid",
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
"policy_engine_mode",
|
||||
"user_matching_mode",
|
||||
"group_matching_mode",
|
||||
"bot_username",
|
||||
"request_message_access",
|
||||
]
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
@@ -0,0 +1,33 @@
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionSerializer,
|
||||
GroupSourceConnectionViewSet,
|
||||
UserSourceConnectionSerializer,
|
||||
UserSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.sources.telegram.models import (
|
||||
GroupTelegramSourceConnection,
|
||||
UserTelegramSourceConnection,
|
||||
)
|
||||
|
||||
|
||||
class UserTelegramSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserTelegramSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields
|
||||
|
||||
|
||||
class UserTelegramSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = UserTelegramSourceConnection.objects.all()
|
||||
serializer_class = UserTelegramSourceConnectionSerializer
|
||||
|
||||
|
||||
class GroupTelegramSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupTelegramSourceConnection
|
||||
|
||||
|
||||
class GroupTelegramSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
|
||||
queryset = GroupTelegramSourceConnection.objects.all()
|
||||
serializer_class = GroupTelegramSourceConnectionSerializer
|
||||
@@ -0,0 +1,9 @@
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class TelegramConfig(ManagedAppConfig):
|
||||
name = "authentik.sources.telegram"
|
||||
label = "authentik_sources_telegram"
|
||||
verbose_name = "authentik Sources.Telegram"
|
||||
mountpoint = "source/telegram/"
|
||||
default = True
|
||||
@@ -0,0 +1,118 @@
|
||||
# Generated by Django 5.1.12 on 2025-09-24 07:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0050_user_last_updated_and_more"),
|
||||
("authentik_flows", "0028_flowtoken_revoke_on_execution"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GroupTelegramSourceConnection",
|
||||
fields=[
|
||||
(
|
||||
"groupsourceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.groupsourceconnection",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Group Telegram Source Connection",
|
||||
"verbose_name_plural": "Group Telegram Source Connections",
|
||||
},
|
||||
bases=("authentik_core.groupsourceconnection",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TelegramSourcePropertyMapping",
|
||||
fields=[
|
||||
(
|
||||
"propertymapping_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.propertymapping",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Telegram Source Property Mapping",
|
||||
"verbose_name_plural": "Telegram Source Property Mappings",
|
||||
},
|
||||
bases=("authentik_core.propertymapping",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserTelegramSourceConnection",
|
||||
fields=[
|
||||
(
|
||||
"usersourceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.usersourceconnection",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User Telegram Source Connection",
|
||||
"verbose_name_plural": "User Telegram Source Connections",
|
||||
},
|
||||
bases=("authentik_core.usersourceconnection",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TelegramSource",
|
||||
fields=[
|
||||
(
|
||||
"source_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.source",
|
||||
),
|
||||
),
|
||||
("bot_username", models.TextField(help_text="Telegram bot username")),
|
||||
("bot_token", models.TextField(help_text="Telegram bot token")),
|
||||
(
|
||||
"request_message_access",
|
||||
models.BooleanField(
|
||||
default=False, help_text="Request access to send messages from your bot."
|
||||
),
|
||||
),
|
||||
(
|
||||
"pre_authentication_flow",
|
||||
models.ForeignKey(
|
||||
help_text="Flow used before authentication.",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="telegram_source_pre_authentication",
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Telegram Source",
|
||||
"verbose_name_plural": "Telegram Sources",
|
||||
},
|
||||
bases=("authentik_core.source",),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Telegram source"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import BaseSerializer, Serializer
|
||||
|
||||
from authentik.core.models import (
|
||||
GroupSourceConnection,
|
||||
PropertyMapping,
|
||||
Source,
|
||||
UserSourceConnection,
|
||||
)
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.challenge import RedirectChallenge
|
||||
from authentik.flows.models import Flow
|
||||
|
||||
|
||||
class TelegramSource(Source):
|
||||
"""Log in with Telegram."""
|
||||
|
||||
bot_username = models.TextField(help_text=_("Telegram bot username"))
|
||||
bot_token = models.TextField(help_text=_("Telegram bot token"))
|
||||
|
||||
request_message_access = models.BooleanField(
|
||||
default=False, help_text=_("Request access to send messages from your bot.")
|
||||
)
|
||||
|
||||
pre_authentication_flow = models.ForeignKey(
|
||||
Flow,
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_("Flow used before authentication."),
|
||||
related_name="telegram_source_pre_authentication",
|
||||
)
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-source-telegram-form"
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
icon = super().icon_url
|
||||
if not icon:
|
||||
icon = static("authentik/sources/telegram.svg")
|
||||
return icon
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.sources.telegram.api.source import TelegramSourceSerializer
|
||||
|
||||
return TelegramSourceSerializer
|
||||
|
||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
challenge=RedirectChallenge(
|
||||
data={
|
||||
"to": reverse(
|
||||
"authentik_sources_telegram:start",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
}
|
||||
),
|
||||
name=self.name,
|
||||
icon_url=self.icon_url,
|
||||
)
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.name,
|
||||
"component": "ak-user-settings-source-telegram",
|
||||
"icon_url": self.icon_url,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||
return TelegramSourcePropertyMapping
|
||||
|
||||
def get_base_user_properties(
|
||||
self, info: dict[str, Any] | None = None, **kwargs
|
||||
) -> dict[str, Any | dict[str, Any]]:
|
||||
info = info or {}
|
||||
name = info.get("first_name", "")
|
||||
if "last_name" in info:
|
||||
name += " " + info["last_name"]
|
||||
return {
|
||||
"username": info.get("username", None),
|
||||
"email": None,
|
||||
"name": name if name else None,
|
||||
}
|
||||
|
||||
def get_base_group_properties(self, group_id: str, **kwargs):
|
||||
return {
|
||||
"name": group_id,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Telegram Source")
|
||||
verbose_name_plural = _("Telegram Sources")
|
||||
|
||||
|
||||
class TelegramSourcePropertyMapping(PropertyMapping):
|
||||
"""Map Telegram properties to User or Group object attributes"""
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-property-mapping-source-telegram-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.telegram.api.property_mappings import (
|
||||
TelegramSourcePropertyMappingSerializer,
|
||||
)
|
||||
|
||||
return TelegramSourcePropertyMappingSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Telegram Source Property Mapping")
|
||||
verbose_name_plural = _("Telegram Source Property Mappings")
|
||||
|
||||
|
||||
class UserTelegramSourceConnection(UserSourceConnection):
|
||||
"""Connect user and Telegram source"""
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.telegram.api.source_connection import (
|
||||
UserTelegramSourceConnectionSerializer,
|
||||
)
|
||||
|
||||
return UserTelegramSourceConnectionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User Telegram Source Connection")
|
||||
verbose_name_plural = _("User Telegram Source Connections")
|
||||
|
||||
|
||||
class GroupTelegramSourceConnection(GroupSourceConnection):
|
||||
"""Group-source connection for Telegram"""
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.telegram.api.source_connection import (
|
||||
GroupTelegramSourceConnectionSerializer,
|
||||
)
|
||||
|
||||
return GroupTelegramSourceConnectionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Group Telegram Source Connection")
|
||||
verbose_name_plural = _("Group Telegram Source Connections")
|
||||
@@ -0,0 +1,48 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.fields import BooleanField, CharField, IntegerField, URLField
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||
from authentik.stages.identification.stage import LoginChallengeMixin
|
||||
|
||||
|
||||
class TelegramLoginChallenge(LoginChallengeMixin, Challenge):
|
||||
component = CharField(default="ak-source-telegram")
|
||||
bot_username = CharField(help_text=_("Telegram bot username"))
|
||||
request_message_access = BooleanField()
|
||||
|
||||
|
||||
class TelegramChallengeResponse(ChallengeResponse):
|
||||
component = CharField(default="ak-source-telegram")
|
||||
|
||||
id = IntegerField()
|
||||
first_name = CharField(max_length=255, required=False)
|
||||
last_name = CharField(max_length=255, required=False)
|
||||
username = CharField(max_length=255, required=False)
|
||||
photo_url = URLField(required=False)
|
||||
auth_date = IntegerField(required=True)
|
||||
hash = CharField(max_length=64, required=True)
|
||||
|
||||
def validate_auth_date(self, auth_date: int) -> int:
|
||||
if datetime.fromtimestamp(auth_date) < datetime.now() - timedelta(minutes=5):
|
||||
raise ValidationError(_("Authentication date is too old"))
|
||||
return auth_date
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
# Check the response as defined in https://core.telegram.org/widgets/login
|
||||
attrs_to_check = attrs.copy()
|
||||
attrs_to_check.pop("component")
|
||||
attrs_to_check.pop("hash")
|
||||
check_str = "\n".join([f"{key}={value}" for key, value in sorted(attrs_to_check.items())])
|
||||
digest = hmac.new(
|
||||
hashlib.sha256(self.stage.source.bot_token.encode("utf-8")).digest(),
|
||||
check_str.encode("utf-8"),
|
||||
"sha256",
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(digest, attrs["hash"]):
|
||||
raise ValidationError(_("Invalid hash"))
|
||||
return attrs
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Telegram source tests"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.sources.telegram.stage import TelegramChallengeResponse
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
|
||||
|
||||
class MockTelegramResponseMixin:
|
||||
def _add_hash(self, response):
|
||||
to_hash = "\n".join([f"{key}={value}" for key, value in sorted(response.items())])
|
||||
response["hash"] = hmac.new(
|
||||
hashlib.sha256(self.source.bot_token.encode("utf-8")).digest(),
|
||||
to_hash.encode("utf-8"),
|
||||
"sha256",
|
||||
).hexdigest()
|
||||
|
||||
def _make_valid_response(self):
|
||||
resp = {
|
||||
"id": "123456789",
|
||||
"first_name": "Test",
|
||||
"last_name": "User",
|
||||
"username": "testuser",
|
||||
"auth_date": str(int(datetime.now().timestamp())),
|
||||
}
|
||||
self._add_hash(resp)
|
||||
return resp
|
||||
|
||||
def _make_outdated_response(self):
|
||||
resp = self._make_valid_response()
|
||||
resp["auth_date"] = str(int((datetime.now() - timedelta(days=1)).timestamp()))
|
||||
self._add_hash(resp)
|
||||
return resp
|
||||
|
||||
|
||||
class TestTelegramSource(MockTelegramResponseMixin, TestCase):
|
||||
"""Telegram Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
from authentik.sources.telegram.models import TelegramSource
|
||||
|
||||
self.source = TelegramSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
bot_username="test_bot",
|
||||
bot_token="modern_token", # nosec
|
||||
request_message_access=True,
|
||||
pre_authentication_flow=create_test_flow(),
|
||||
)
|
||||
self.mock_stage = Mock()
|
||||
self.mock_stage.source = self.source
|
||||
|
||||
def test_ui_login_button(self):
|
||||
"""Test UI login button"""
|
||||
ui_login_button = self.source.ui_login_button(None)
|
||||
self.assertIsNotNone(ui_login_button)
|
||||
self.assertEqual(ui_login_button.name, "test")
|
||||
self.assertTrue(ui_login_button.challenge.is_valid(raise_exception=True))
|
||||
|
||||
def test_challenge_response(self):
|
||||
"""Test correct Telegram response validation"""
|
||||
cr = TelegramChallengeResponse(data=self._make_valid_response())
|
||||
cr.stage = self.mock_stage
|
||||
self.assertTrue(cr.is_valid(raise_exception=True))
|
||||
|
||||
def test_outdated_challenge_response(self):
|
||||
"""Test outdated Telegram response validation"""
|
||||
cr = TelegramChallengeResponse(data=self._make_outdated_response())
|
||||
cr.stage = self.mock_stage
|
||||
with self.assertRaises(ValidationError):
|
||||
cr.is_valid(raise_exception=True)
|
||||
|
||||
def test_invalid_hash_challenge_response(self):
|
||||
"""Test invalid hash in Telegram response validation"""
|
||||
resp = self._make_valid_response()
|
||||
resp["hash"] = "invalid_hash"
|
||||
cr = TelegramChallengeResponse(data=resp)
|
||||
cr.stage = self.mock_stage
|
||||
with self.assertRaises(ValidationError):
|
||||
cr.is_valid(raise_exception=True)
|
||||
|
||||
def test_user_base_properties(self):
|
||||
"""Test user base properties"""
|
||||
cr = TelegramChallengeResponse(data=self._make_valid_response())
|
||||
cr.stage = self.mock_stage
|
||||
cr.is_valid(raise_exception=True)
|
||||
properties = self.source.get_base_user_properties(info=cr.validated_data)
|
||||
self.assertEqual(
|
||||
properties,
|
||||
{
|
||||
"username": "testuser",
|
||||
"name": "Test User",
|
||||
"email": None,
|
||||
},
|
||||
)
|
||||
|
||||
def test_group_base_properties(self):
|
||||
"""Test group base properties"""
|
||||
for group_id in ["group 1", "group 2"]:
|
||||
properties = self.source.get_base_group_properties(group_id=group_id)
|
||||
self.assertEqual(properties, {"name": group_id})
|
||||
|
||||
|
||||
class TestTelegramViews(MockTelegramResponseMixin, FlowTestCase):
|
||||
"""Test Telegram source views"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
from authentik.sources.telegram.models import TelegramSource
|
||||
|
||||
self.pre_auth_flow = create_test_flow()
|
||||
|
||||
self.source = TelegramSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
bot_username="test_bot",
|
||||
bot_token="modern_token", # nosec
|
||||
request_message_access=True,
|
||||
enrollment_flow=create_test_flow(),
|
||||
pre_authentication_flow=self.pre_auth_flow,
|
||||
)
|
||||
|
||||
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||
self.stage = IdentificationStage.objects.create(
|
||||
name="identification",
|
||||
user_fields=[UserFields.E_MAIL],
|
||||
pretend_user_exists=False,
|
||||
)
|
||||
self.stage.sources.set([self.source])
|
||||
self.stage.save()
|
||||
FlowStageBinding.objects.create(
|
||||
target=self.flow,
|
||||
stage=self.stage,
|
||||
order=0,
|
||||
)
|
||||
|
||||
def _make_initial_request(self):
|
||||
return self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
)
|
||||
|
||||
def _make_start_request(self):
|
||||
return self.client.get(
|
||||
reverse("authentik_sources_telegram:start", kwargs={"source_slug": self.source.slug}),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
def test_start_view(self):
|
||||
"""Test TelegramStartView"""
|
||||
self.assertEqual(self._make_initial_request().status_code, 200)
|
||||
|
||||
response = self._make_start_request()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
response.redirect_chain[0][0],
|
||||
reverse("authentik_core:if-flow", kwargs={"flow_slug": self.pre_auth_flow.slug}),
|
||||
)
|
||||
|
||||
def test_challenge_view(self):
|
||||
"""Test TelegramLoginView"""
|
||||
self._make_initial_request()
|
||||
self._make_start_request()
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.pre_auth_flow.slug})
|
||||
get_response = self.client.get(url)
|
||||
self.assertEqual(get_response.status_code, 200)
|
||||
form_data = self._make_valid_response()
|
||||
form_data["component"] = "ak-source-telegram"
|
||||
response = self.client.post(url, form_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(
|
||||
response,
|
||||
reverse(
|
||||
"authentik_core:if-flow", kwargs={"flow_slug": self.source.enrollment_flow.slug}
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Telegram source API views"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.sources.telegram.api.property_mappings import TelegramSourcePropertyMappingViewSet
|
||||
from authentik.sources.telegram.api.source import TelegramSourceViewSet
|
||||
from authentik.sources.telegram.api.source_connection import (
|
||||
GroupTelegramSourceConnectionViewSet,
|
||||
UserTelegramSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.sources.telegram.views import TelegramLoginView, TelegramStartView
|
||||
|
||||
urlpatterns = [
|
||||
path("<slug:source_slug>/start/", TelegramStartView.as_view(), name="start"),
|
||||
path("<slug:source_slug>/", TelegramLoginView.as_view(), name="login"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("propertymappings/source/telegram", TelegramSourcePropertyMappingViewSet),
|
||||
("sources/user_connections/telegram", UserTelegramSourceConnectionViewSet),
|
||||
("sources/group_connections/telegram", GroupTelegramSourceConnectionViewSet),
|
||||
("sources/telegram", TelegramSourceViewSet),
|
||||
]
|
||||
@@ -0,0 +1,98 @@
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.flows.challenge import Challenge
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import in_memory_stage
|
||||
from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_REDIRECT,
|
||||
PLAN_CONTEXT_SOURCE,
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||
from authentik.sources.telegram.models import (
|
||||
GroupTelegramSourceConnection,
|
||||
TelegramSource,
|
||||
UserTelegramSourceConnection,
|
||||
)
|
||||
from authentik.sources.telegram.stage import TelegramChallengeResponse, TelegramLoginChallenge
|
||||
|
||||
|
||||
class TelegramStartView(View):
|
||||
def handle_login_flow(
|
||||
self, source: TelegramSource, *stages_to_append, **kwargs
|
||||
) -> HttpResponse:
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
# authorize application
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
)
|
||||
kwargs.update(
|
||||
{
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
}
|
||||
)
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
planner = FlowPlanner(source.pre_authentication_flow)
|
||||
planner.allow_empty_flows = True
|
||||
try:
|
||||
plan = planner.plan(self.request, kwargs)
|
||||
except FlowNonApplicableException:
|
||||
raise Http404 from None
|
||||
for stage in stages_to_append:
|
||||
plan.append_stage(stage)
|
||||
return plan.to_redirect(self.request, source.pre_authentication_flow)
|
||||
|
||||
def get(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
source = get_object_or_404(TelegramSource, slug=source_slug, enabled=True)
|
||||
telegram_login_stage = in_memory_stage(TelegramLoginView)
|
||||
|
||||
return self.handle_login_flow(source, telegram_login_stage)
|
||||
|
||||
|
||||
class TelegramSourceFlowManager(SourceFlowManager):
|
||||
"""Flow manager for Telegram source"""
|
||||
|
||||
user_connection_type = UserTelegramSourceConnection
|
||||
group_connection_type = GroupTelegramSourceConnection
|
||||
|
||||
|
||||
class TelegramLoginView(ChallengeStageView):
|
||||
|
||||
response_class = TelegramChallengeResponse
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.source = self.executor.plan.context[PLAN_CONTEXT_SOURCE]
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
return TelegramLoginChallenge(
|
||||
data={
|
||||
"bot_username": self.source.bot_username,
|
||||
"request_message_access": self.source.request_message_access,
|
||||
},
|
||||
)
|
||||
|
||||
def challenge_valid(self, response: TelegramChallengeResponse) -> HttpResponse:
|
||||
raw_info = response.validated_data.copy()
|
||||
raw_info.pop("component")
|
||||
raw_info.pop("hash")
|
||||
raw_info.pop("auth_date")
|
||||
source = self.source
|
||||
sfm = TelegramSourceFlowManager(
|
||||
source=source,
|
||||
request=self.request,
|
||||
identifier=raw_info["id"],
|
||||
user_info={"info": raw_info},
|
||||
policy_context={"telegram": raw_info},
|
||||
)
|
||||
return sfm.get_flow(
|
||||
raw_info=raw_info,
|
||||
)
|
||||
@@ -3016,6 +3016,166 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_telegram.grouptelegramsourceconnection"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"created",
|
||||
"must_created",
|
||||
"present"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.grouptelegramsourceconnection_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.grouptelegramsourceconnection"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.grouptelegramsourceconnection"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_telegram.telegramsource"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"created",
|
||||
"must_created",
|
||||
"present"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.telegramsource_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.telegramsource"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.telegramsource"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_telegram.telegramsourcepropertymapping"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"created",
|
||||
"must_created",
|
||||
"present"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.telegramsourcepropertymapping_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.telegramsourcepropertymapping"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.telegramsourcepropertymapping"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_telegram.usertelegramsourceconnection"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"created",
|
||||
"must_created",
|
||||
"present"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.usertelegramsourceconnection_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.usertelegramsourceconnection"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_telegram.usertelegramsourceconnection"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -5271,6 +5431,22 @@
|
||||
"authentik_sources_scim.view_scimsourcegroup",
|
||||
"authentik_sources_scim.view_scimsourcepropertymapping",
|
||||
"authentik_sources_scim.view_scimsourceuser",
|
||||
"authentik_sources_telegram.add_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.add_telegramsource",
|
||||
"authentik_sources_telegram.add_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.add_usertelegramsourceconnection",
|
||||
"authentik_sources_telegram.change_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.change_telegramsource",
|
||||
"authentik_sources_telegram.change_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.change_usertelegramsourceconnection",
|
||||
"authentik_sources_telegram.delete_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.delete_telegramsource",
|
||||
"authentik_sources_telegram.delete_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.delete_usertelegramsourceconnection",
|
||||
"authentik_sources_telegram.view_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.view_telegramsource",
|
||||
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
||||
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.add_duodevice",
|
||||
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
||||
@@ -7336,6 +7512,7 @@
|
||||
"authentik.sources.plex",
|
||||
"authentik.sources.saml",
|
||||
"authentik.sources.scim",
|
||||
"authentik.sources.telegram",
|
||||
"authentik.stages.authenticator",
|
||||
"authentik.stages.authenticator_duo",
|
||||
"authentik.stages.authenticator_email",
|
||||
@@ -7446,6 +7623,10 @@
|
||||
"authentik_sources_saml.groupsamlsourceconnection",
|
||||
"authentik_sources_scim.scimsource",
|
||||
"authentik_sources_scim.scimsourcepropertymapping",
|
||||
"authentik_sources_telegram.telegramsource",
|
||||
"authentik_sources_telegram.telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.usertelegramsourceconnection",
|
||||
"authentik_sources_telegram.grouptelegramsourceconnection",
|
||||
"authentik_stages_authenticator_duo.authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.duodevice",
|
||||
"authentik_stages_authenticator_email.authenticatoremailstage",
|
||||
@@ -9976,6 +10157,22 @@
|
||||
"authentik_sources_scim.view_scimsourcegroup",
|
||||
"authentik_sources_scim.view_scimsourcepropertymapping",
|
||||
"authentik_sources_scim.view_scimsourceuser",
|
||||
"authentik_sources_telegram.add_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.add_telegramsource",
|
||||
"authentik_sources_telegram.add_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.add_usertelegramsourceconnection",
|
||||
"authentik_sources_telegram.change_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.change_telegramsource",
|
||||
"authentik_sources_telegram.change_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.change_usertelegramsourceconnection",
|
||||
"authentik_sources_telegram.delete_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.delete_telegramsource",
|
||||
"authentik_sources_telegram.delete_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.delete_usertelegramsourceconnection",
|
||||
"authentik_sources_telegram.view_grouptelegramsourceconnection",
|
||||
"authentik_sources_telegram.view_telegramsource",
|
||||
"authentik_sources_telegram.view_telegramsourcepropertymapping",
|
||||
"authentik_sources_telegram.view_usertelegramsourceconnection",
|
||||
"authentik_stages_authenticator_duo.add_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.add_duodevice",
|
||||
"authentik_stages_authenticator_duo.change_authenticatorduostage",
|
||||
@@ -12068,6 +12265,289 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_telegram.grouptelegramsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Group"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Icon"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_telegram.grouptelegramsourceconnection_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_grouptelegramsourceconnection",
|
||||
"change_grouptelegramsourceconnection",
|
||||
"delete_grouptelegramsourceconnection",
|
||||
"view_grouptelegramsourceconnection"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_telegram.telegramsource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name",
|
||||
"description": "Source's display Name."
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 1,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$",
|
||||
"title": "Slug",
|
||||
"description": "Internal source name, used in URLs."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"title": "Enabled"
|
||||
},
|
||||
"authentication_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Authentication flow",
|
||||
"description": "Flow to use when authenticating existing users."
|
||||
},
|
||||
"enrollment_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Enrollment flow",
|
||||
"description": "Flow to use when enrolling new users."
|
||||
},
|
||||
"user_property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "User property mappings"
|
||||
},
|
||||
"group_property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "Group property mappings"
|
||||
},
|
||||
"policy_engine_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"all",
|
||||
"any"
|
||||
],
|
||||
"title": "Policy engine mode"
|
||||
},
|
||||
"user_matching_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"identifier",
|
||||
"email_link",
|
||||
"email_deny",
|
||||
"username_link",
|
||||
"username_deny"
|
||||
],
|
||||
"title": "User matching mode",
|
||||
"description": "How the source determines if an existing user should be authenticated or a new user enrolled."
|
||||
},
|
||||
"user_path_template": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "User path template"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Icon"
|
||||
},
|
||||
"bot_username": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Bot username",
|
||||
"description": "Telegram bot username"
|
||||
},
|
||||
"bot_token": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Bot token",
|
||||
"description": "Telegram bot token"
|
||||
},
|
||||
"request_message_access": {
|
||||
"type": "boolean",
|
||||
"title": "Request message access",
|
||||
"description": "Request access to send messages from your bot."
|
||||
},
|
||||
"pre_authentication_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Pre authentication flow",
|
||||
"description": "Flow used before authentication."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_telegram.telegramsource_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_telegramsource",
|
||||
"change_telegramsource",
|
||||
"delete_telegramsource",
|
||||
"view_telegramsource"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_telegram.telegramsourcepropertymapping": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"managed": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"minLength": 1,
|
||||
"title": "Managed by authentik",
|
||||
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Expression"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_telegram.telegramsourcepropertymapping_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_telegramsourcepropertymapping",
|
||||
"change_telegramsourcepropertymapping",
|
||||
"delete_telegramsourcepropertymapping",
|
||||
"view_telegramsourcepropertymapping"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_telegram.usertelegramsourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "integer",
|
||||
"title": "User"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"title": "Source"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Icon"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_telegram.usertelegramsourceconnection_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_usertelegramsourceconnection",
|
||||
"change_usertelegramsourceconnection",
|
||||
"delete_usertelegramsourceconnection",
|
||||
"view_usertelegramsourceconnection"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_authenticator_duo.authenticatorduostage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -236,6 +236,7 @@ module = [
|
||||
"authentik.sources.plex.*",
|
||||
"authentik.sources.saml.*",
|
||||
"authentik.sources.scim.*",
|
||||
"authentik.sources.telegram.*",
|
||||
"authentik.stages.authenticator_duo.*",
|
||||
"authentik.stages.authenticator_email.*",
|
||||
"authentik.stages.authenticator_sms.*",
|
||||
|
||||
+1662
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs
|
||||
id="defs121">
|
||||
<linearGradient
|
||||
x1="0.5"
|
||||
y1="0"
|
||||
x2="0.5"
|
||||
y2="0.99258339"
|
||||
id="linearGradient-1">
|
||||
<stop
|
||||
stop-color="#2AABEE"
|
||||
offset="0%"
|
||||
id="stop116" />
|
||||
<stop
|
||||
stop-color="#229ED9"
|
||||
offset="100%"
|
||||
id="stop118" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(-488,-488)">
|
||||
<circle
|
||||
id="Oval"
|
||||
fill="url(#linearGradient-1)"
|
||||
cx="500"
|
||||
cy="500"
|
||||
r="12"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.024" />
|
||||
<path
|
||||
d="m 493.43188,499.87333 c 3.49825,-1.52413 5.83096,-2.52893 6.99813,-3.0144 3.33253,-1.38611 4.025,-1.62689 4.47634,-1.63485 0.0993,-0.002 0.32123,0.0229 0.465,0.13952 0.12141,0.0985 0.15481,0.23158 0.17079,0.32498 0.016,0.0934 0.0359,0.30615 0.0201,0.4724 -0.18059,1.89748 -0.96201,6.50217 -1.35955,8.62739 -0.16821,0.89925 -0.49943,1.20077 -0.82009,1.23028 -0.69686,0.0641 -1.22602,-0.46054 -1.90097,-0.90297 -1.05615,-0.69233 -1.65282,-1.1233 -2.678,-1.79888 -1.18477,-0.78075 -0.41673,-1.20986 0.25847,-1.91115 0.1767,-0.18354 3.24709,-2.97629 3.30652,-3.22964 0.007,-0.0317 0.0143,-0.14979 -0.0558,-0.21216 -0.0702,-0.0624 -0.17372,-0.041 -0.24845,-0.0241 -0.10593,0.024 -1.79315,1.13924 -5.06167,3.34558 -0.47891,0.32886 -0.9127,0.48909 -1.30135,0.48069 -0.42847,-0.009 -1.25265,-0.24226 -1.86535,-0.44142 -0.7515,-0.24429 -1.34878,-0.37344 -1.29677,-0.78831 0.0271,-0.21609 0.32466,-0.43708 0.89272,-0.66298 z"
|
||||
id="Path-3"
|
||||
fill="#ffffff"
|
||||
style="stroke-width:0.024" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -12,6 +12,7 @@ import "#admin/property-mappings/PropertyMappingSourceOAuthForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourcePlexForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourceSAMLForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourceSCIMForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourceTelegramForm";
|
||||
import "#admin/property-mappings/PropertyMappingTestForm";
|
||||
import "#admin/property-mappings/PropertyMappingWizard";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { BasePropertyMappingForm } from "#admin/property-mappings/BasePropertyMappingForm";
|
||||
|
||||
import { PropertymappingsApi, TelegramSourcePropertyMapping } from "@goauthentik/api";
|
||||
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-property-mapping-source-telegram-form")
|
||||
export class PropertyMappingSourceTelegramForm extends BasePropertyMappingForm<TelegramSourcePropertyMapping> {
|
||||
protected override docLink = "/users-sources/sources/property-mappings/expressions";
|
||||
|
||||
loadInstance(pk: string): Promise<TelegramSourcePropertyMapping> {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceTelegramRetrieve({
|
||||
pmUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: TelegramSourcePropertyMapping): Promise<TelegramSourcePropertyMapping> {
|
||||
if (this.instance) {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceTelegramUpdate({
|
||||
pmUuid: this.instance.pk,
|
||||
telegramSourcePropertyMappingRequest: data,
|
||||
});
|
||||
}
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceTelegramCreate({
|
||||
telegramSourcePropertyMappingRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-property-mapping-source-telegram-form": PropertyMappingSourceTelegramForm;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import "#admin/property-mappings/PropertyMappingSourceOAuthForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourcePlexForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourceSAMLForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourceSCIMForm";
|
||||
import "#admin/property-mappings/PropertyMappingSourceTelegramForm";
|
||||
import "#admin/property-mappings/PropertyMappingTestForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#elements/wizard/FormWizardPage";
|
||||
|
||||
@@ -4,6 +4,7 @@ import "#admin/sources/oauth/OAuthSourceViewPage";
|
||||
import "#admin/sources/plex/PlexSourceViewPage";
|
||||
import "#admin/sources/saml/SAMLSourceViewPage";
|
||||
import "#admin/sources/scim/SCIMSourceViewPage";
|
||||
import "#admin/sources/telegram/TelegramSourceViewPage";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/buttons/SpinnerButton/ak-spinner-button";
|
||||
|
||||
@@ -63,6 +64,10 @@ export class SourceViewPage extends AKElement {
|
||||
return html`<ak-source-scim-view
|
||||
sourceSlug=${this.source.slug}
|
||||
></ak-source-scim-view>`;
|
||||
case "ak-source-telegram-form":
|
||||
return html`<ak-source-telegram-view
|
||||
sourceSlug=${this.source.slug}
|
||||
></ak-source-telegram-view>`;
|
||||
default:
|
||||
return html`<p>Invalid source type ${this.source.component}</p>`;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import "#admin/sources/oauth/OAuthSourceForm";
|
||||
import "#admin/sources/plex/PlexSourceForm";
|
||||
import "#admin/sources/saml/SAMLSourceForm";
|
||||
import "#admin/sources/scim/SCIMSourceForm";
|
||||
import "#admin/sources/telegram/TelegramSourceForm";
|
||||
import "#elements/forms/ProxyForm";
|
||||
import "#elements/wizard/FormWizardPage";
|
||||
import "#elements/wizard/Wizard";
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { propertyMappingsProvider, propertyMappingsSelector } from "./TelegramSourceFormHelpers.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
|
||||
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
|
||||
import { BaseSourceForm } from "#admin/sources/BaseSourceForm";
|
||||
import { UserMatchingModeToLabel } from "#admin/sources/oauth/utils";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
SourcesApi,
|
||||
TelegramSource,
|
||||
TelegramSourceRequest,
|
||||
UserMatchingModeEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-source-telegram-form")
|
||||
export class TelegramSourceForm extends WithCapabilitiesConfig(BaseSourceForm<TelegramSource>) {
|
||||
async loadInstance(pk: string): Promise<TelegramSource> {
|
||||
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesTelegramRetrieve({
|
||||
slug: pk,
|
||||
});
|
||||
return source;
|
||||
}
|
||||
|
||||
async send(data: TelegramSource): Promise<TelegramSource> {
|
||||
let source: TelegramSource;
|
||||
if (this.instance?.pk) {
|
||||
source = await new SourcesApi(DEFAULT_CONFIG).sourcesTelegramPartialUpdate({
|
||||
slug: this.instance.slug,
|
||||
patchedTelegramSourceRequest: data,
|
||||
});
|
||||
} else {
|
||||
source = await new SourcesApi(DEFAULT_CONFIG).sourcesTelegramCreate({
|
||||
telegramSourceRequest: data as unknown as TelegramSourceRequest,
|
||||
});
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
value=${ifDefined(this.instance?.slug)}
|
||||
label=${msg("Slug")}
|
||||
required
|
||||
input-hint="code"
|
||||
></ak-slug-input>
|
||||
|
||||
<ak-form-element-horizontal name="enabled">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.enabled ?? true}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${msg("Enabled")}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("User matching mode")}
|
||||
required
|
||||
name="userMatchingMode"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=${UserMatchingModeEnum.Identifier}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.Identifier}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.Identifier)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.EmailLink}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.EmailLink}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailLink)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.EmailDeny}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.EmailDeny}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailDeny)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.UsernameLink}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.UsernameLink}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameLink)}
|
||||
</option>
|
||||
<option
|
||||
value=${UserMatchingModeEnum.UsernameDeny}
|
||||
?selected=${this.instance?.userMatchingMode ===
|
||||
UserMatchingModeEnum.UsernameDeny}
|
||||
>
|
||||
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameDeny)}
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Bot username")} required name="botUsername">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.botUsername)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-secret-text-input
|
||||
label=${msg("Bot token")}
|
||||
name="botToken"
|
||||
input-hint="code"
|
||||
?required=${this.instance === undefined}
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-secret-text-input>
|
||||
<ak-form-element-horizontal required name="requestMessageAccess">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${this.instance?.requestMessageAccess ?? true}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Request access to send messages from your bot")}
|
||||
</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group label="${msg("Flow settings")}">
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Pre-authentication flow")}
|
||||
required
|
||||
name="preAuthenticationFlow"
|
||||
>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.StageConfiguration}
|
||||
.currentFlow=${this.instance?.preAuthenticationFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-pre-authentication"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used before authentication.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-authentication"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when authenticating existing users.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Enrollment flow")}
|
||||
name="enrollmentFlow"
|
||||
>
|
||||
<ak-source-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
|
||||
.currentFlow=${this.instance?.enrollmentFlow}
|
||||
.instanceId=${this.instance?.pk}
|
||||
fallback="default-source-enrollment"
|
||||
></ak-source-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow to use when enrolling new users.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group open label="${msg("Telegram Attribute mapping")}">
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("User Property Mappings")}
|
||||
name="userPropertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${propertyMappingsProvider}
|
||||
.selector=${propertyMappingsSelector(
|
||||
this.instance?.userPropertyMappings,
|
||||
)}
|
||||
available-label="${msg("Available User Property Mappings")}"
|
||||
selected-label="${msg("Selected User Property Mappings")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings for user creation.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Group Property Mappings")}
|
||||
name="groupPropertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${propertyMappingsProvider}
|
||||
.selector=${propertyMappingsSelector(
|
||||
this.instance?.groupPropertyMappings,
|
||||
)}
|
||||
available-label="${msg("Available Group Property Mappings")}"
|
||||
selected-label="${msg("Selected Group Property Mappings")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings for group creation.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group label="${msg("Advanced settings")} ">
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
name="policyEngineMode"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${policyEngineModes}
|
||||
.value=${this.instance?.policyEngineMode}
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-source-telegram-form": TelegramSourceForm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { DualSelectPair } from "#elements/ak-dual-select/types";
|
||||
|
||||
import { PropertymappingsApi, TelegramSourcePropertyMapping } from "@goauthentik/api";
|
||||
|
||||
const mappingToSelect = (m: TelegramSourcePropertyMapping) => [m.pk, m.name, m.name, m];
|
||||
|
||||
export async function propertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsSourceTelegramList({
|
||||
ordering: "managed",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map(mappingToSelect),
|
||||
};
|
||||
}
|
||||
|
||||
export function propertyMappingsSelector(instanceMappings?: string[]) {
|
||||
if (!instanceMappings) {
|
||||
return async (mappings: DualSelectPair<TelegramSourcePropertyMapping>[]) =>
|
||||
mappings.filter(
|
||||
([_0, _1, _2, _3]: DualSelectPair<TelegramSourcePropertyMapping>) => false,
|
||||
);
|
||||
}
|
||||
|
||||
return async () => {
|
||||
const pm = new PropertymappingsApi(DEFAULT_CONFIG);
|
||||
const mappings = await Promise.allSettled(
|
||||
instanceMappings.map((instanceId) =>
|
||||
pm.propertymappingsSourceTelegramRetrieve({ pmUuid: instanceId }),
|
||||
),
|
||||
);
|
||||
|
||||
return mappings
|
||||
.filter((s) => s.status === "fulfilled")
|
||||
.map((s) => s.value)
|
||||
.map(mappingToSelect);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import "#admin/sources/telegram/TelegramSourceForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { sourceBindingTypeNotices } from "#admin/sources/utils";
|
||||
|
||||
import {
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
SourcesApi,
|
||||
TelegramSource,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-source-telegram-view")
|
||||
export class TelegramSourceViewPage extends AKElement {
|
||||
@property({ type: String })
|
||||
set sourceSlug(value: string) {
|
||||
new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesTelegramRetrieve({
|
||||
slug: value,
|
||||
})
|
||||
.then((source) => {
|
||||
this.source = source;
|
||||
});
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
source?: TelegramSource;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.source) {
|
||||
return html``;
|
||||
}
|
||||
return html` <ak-tabs>
|
||||
<section
|
||||
slot="page-overview"
|
||||
data-tab-title="${msg("Overview")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list pf-m-2-col-on-lg">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Name")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.source.name}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Telegram bot")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.source.botUsername}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update Telegram Source")} </span>
|
||||
<ak-source-telegram-form
|
||||
slot="form"
|
||||
.instancePk=${this.source.slug}
|
||||
>
|
||||
</ak-source-telegram-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
slot="page-changelog"
|
||||
data-tab-title="${msg("Changelog")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-object-changelog
|
||||
targetModelPk=${this.source.pk || ""}
|
||||
targetModelApp="authentik_sources_telegram"
|
||||
targetModelName="telegramsource"
|
||||
>
|
||||
</ak-object-changelog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
slot="page-policy-binding"
|
||||
data-tab-title="${msg("Policy Bindings")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">
|
||||
${msg(
|
||||
`These bindings control which users can access this source.
|
||||
You can only use policies here as access is checked before the user is authenticated.`,
|
||||
)}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-bound-policies-list
|
||||
.target=${this.source.pk}
|
||||
.typeNotices=${sourceBindingTypeNotices()}
|
||||
.policyEngineMode=${this.source.policyEngineMode}
|
||||
>
|
||||
</ak-bound-policies-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ak-rbac-object-permission-page
|
||||
slot="page-permissions"
|
||||
data-tab-title="${msg("Permissions")}"
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.AuthentikSourcesTelegramTelegramsource}
|
||||
objectPk=${this.source.pk}
|
||||
></ak-rbac-object-permission-page>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-source-telegram-view": TelegramSourceViewPage;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import "#elements/EmptyState";
|
||||
import "#elements/user/sources/SourceSettingsOAuth";
|
||||
import "#elements/user/sources/SourceSettingsPlex";
|
||||
import "#elements/user/sources/SourceSettingsSAML";
|
||||
import "#elements/user/sources/SourceSettingsTelegram";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
@@ -104,6 +105,13 @@ export class UserSourceSettingsPage extends AKElement {
|
||||
.configureUrl=${this.canConnect ? source.configureUrl : undefined}
|
||||
>
|
||||
</ak-user-settings-source-saml>`;
|
||||
case "ak-user-settings-source-telegram":
|
||||
return html`<ak-user-settings-source-telegram
|
||||
objectId=${source.objectUid}
|
||||
title=${source.title}
|
||||
connectionPk=${connectionPk}
|
||||
>
|
||||
</ak-user-settings-source-telegram>`;
|
||||
default:
|
||||
return html`<p>
|
||||
${msg(str`Error: unsupported source settings: ${source.component}`)}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import "#elements/Spinner";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
import { MessageLevel } from "#common/messages";
|
||||
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { BaseUserSettings } from "#elements/user/sources/BaseUserSettings";
|
||||
|
||||
import { SourcesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-settings-source-telegram")
|
||||
export class SourceSettingsTelegram extends BaseUserSettings {
|
||||
@property()
|
||||
title!: string;
|
||||
|
||||
@property({ type: Number })
|
||||
connectionPk = 0;
|
||||
|
||||
render(): TemplateResult {
|
||||
if (this.connectionPk === -1) {
|
||||
return html`<ak-spinner></ak-spinner>`;
|
||||
}
|
||||
if (this.connectionPk > 0) {
|
||||
return html`<button
|
||||
class="pf-c-button pf-m-danger"
|
||||
@click=${() => {
|
||||
return new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesUserConnectionsTelegramDestroy({
|
||||
id: this.connectionPk,
|
||||
})
|
||||
.then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.info,
|
||||
message: msg("Successfully disconnected source"),
|
||||
});
|
||||
})
|
||||
.catch(async (error: unknown) => {
|
||||
const parsedError = await parseAPIResponseError(error);
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg(
|
||||
str`Failed to disconnected source: ${pluckErrorDetail(parsedError)}`,
|
||||
),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.parentElement?.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
${msg("Disconnect")}
|
||||
</button>`;
|
||||
}
|
||||
return html`${msg("-")}`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-user-settings-source-telegram": SourceSettingsTelegram;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import "#flow/components/ak-brand-footer";
|
||||
import "#flow/components/ak-flow-card";
|
||||
import "#flow/sources/apple/AppleLoginInit";
|
||||
import "#flow/sources/plex/PlexLoginInit";
|
||||
import "#flow/sources/telegram/TelegramLogin";
|
||||
import "#flow/stages/FlowErrorStage";
|
||||
import "#flow/stages/FlowFrameStage";
|
||||
import "#flow/stages/RedirectStage";
|
||||
@@ -490,6 +491,11 @@ export class FlowExecutor
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-flow-source-oauth-apple>`;
|
||||
case "ak-source-telegram":
|
||||
return html`<ak-flow-source-telegram
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-flow-source-telegram>`;
|
||||
// Providers
|
||||
case "ak-provider-oauth2-device-code":
|
||||
await import("#flow/providers/oauth2/DeviceCode");
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
|
||||
import { TelegramChallengeResponseRequest, TelegramLoginChallenge } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
type TelegramUserResponse = {
|
||||
id: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
photo_url?: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
@customElement("ak-flow-source-telegram")
|
||||
export class TelegramLogin extends BaseStage<
|
||||
TelegramLoginChallenge,
|
||||
TelegramChallengeResponseRequest
|
||||
> {
|
||||
btnRef = createRef();
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, PFDivider];
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
const widgetScript = document.createElement("script");
|
||||
widgetScript.src = "https://telegram.org/js/telegram-widget.js?22";
|
||||
widgetScript.type = "text/javascript";
|
||||
widgetScript.setAttribute("data-radius", "0");
|
||||
widgetScript.setAttribute("data-telegram-login", this.challenge.botUsername);
|
||||
if (this.challenge.requestMessageAccess) {
|
||||
widgetScript.setAttribute("data-request-access", "write");
|
||||
}
|
||||
const callbackName =
|
||||
"__ak_telegram_login_callback_" + (Math.random() + 1).toString(36).substring(7);
|
||||
(window as unknown as Record<string, (user: TelegramUserResponse) => void>)[callbackName] =
|
||||
(user: TelegramUserResponse) => {
|
||||
this.host.submit({
|
||||
id: user.id,
|
||||
authDate: user.auth_date,
|
||||
hash: user.hash,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
username: user.username,
|
||||
photoUrl: user.photo_url,
|
||||
});
|
||||
};
|
||||
widgetScript.setAttribute("data-onauth", callbackName + "(user)");
|
||||
this.btnRef.value?.appendChild(widgetScript);
|
||||
widgetScript.onload = () => {
|
||||
if (widgetScript.previousSibling) {
|
||||
this.btnRef.value?.appendChild(widgetScript.previousSibling);
|
||||
}
|
||||
};
|
||||
document.body.append(widgetScript);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html` <ak-flow-card .challenge=${this.challenge}>
|
||||
<span slot="title">${msg("Authenticating with Telegram...")}</span>
|
||||
<form class="pf-c-form">
|
||||
<hr class="pf-c-divider" />
|
||||
<p>${msg("Click the button below to start.")}</p>
|
||||
|
||||
<div ${ref(this.btnRef)}></div>
|
||||
</form>
|
||||
</ak-flow-card>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-flow-source-telegram": TelegramLogin;
|
||||
}
|
||||
}
|
||||
@@ -60,16 +60,17 @@ authentik
|
||||
│ ├── oauth2 - OIDC-compliant OAuth2 provider
|
||||
│ ├── proxy - Provides an identity-aware proxy using an outpost
|
||||
│ ├── radius - Provides a RADIUS server that authenticates using flows
|
||||
│ ├── saml - SAML2 Provider
|
||||
│ └── scim - SCIM Provider
|
||||
│ ├── saml - SAML2 provider
|
||||
│ └── scim - SCIM provider
|
||||
├── recovery - Generate keys to use in case you lock yourself out
|
||||
├── root - Root Django application, contains global settings and routes
|
||||
├── sources
|
||||
│ ├── kerberos - Sync Kerberos users into authentik
|
||||
│ ├── ldap - Sync LDAP users from OpenLDAP or Active Directory into authentik
|
||||
│ ├── oauth - OAuth1 and OAuth2 Source
|
||||
│ ├── oauth - OAuth1 and OAuth2 source
|
||||
│ ├── plex - Plex source
|
||||
│ └── saml - SAML2 Source
|
||||
│ ├── saml - SAML2 source
|
||||
│ └── telegram - Telegram source
|
||||
├── stages
|
||||
│ ├── authenticator_duo - Configure a DUO authenticator
|
||||
│ ├── authenticator_static - Configure TOTP backup keys
|
||||
|
||||
@@ -570,8 +570,9 @@ const items = [
|
||||
],
|
||||
},
|
||||
"users-sources/sources/social-logins/mailcow/index",
|
||||
"users-sources/sources/social-logins/twitch/index",
|
||||
"users-sources/sources/social-logins/plex/index",
|
||||
"users-sources/sources/social-logins/telegram/index",
|
||||
"users-sources/sources/social-logins/twitch/index",
|
||||
"users-sources/sources/social-logins/twitter/index",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Telegram
|
||||
support_level: community
|
||||
---
|
||||
|
||||
Configuring Telegram as a source allows users to authenticate within authentik using their Telegram account credentials.
|
||||
|
||||
## Preparation
|
||||
|
||||
Using Telegram as a source requires that your authentik instance is served from a domain.
|
||||
|
||||
## Telegram configuration
|
||||
|
||||
To use Telegram as a source, you first need to register a Telegram bot:
|
||||
|
||||
1. Start a chat with `@BotFather` on Telegram.
|
||||
2. Use the `/newbot` command to create a new bot. Define a name and username for your new bot (e.g., `authentik_bot`).
|
||||
3. BotFather will provide you with a token for the new bot. Take note of the username and token because they will be required when setting up the source in authentik.
|
||||
4. Link the bot to your authentik domain name using the `/setdomain` command.
|
||||
|
||||
:::note
|
||||
The domain name set in Telegram must **exactly** match the FQDN of the authentik installation.
|
||||
:::
|
||||
|
||||
Now that the bot is configured you can proceed to creating a source in authentik.
|
||||
|
||||
## authentik configuration
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Directory** > **Federation and Social login**, click **Create**, and then configure the following settings:
|
||||
- **Select type**: select **Telegram** as the source type.
|
||||
- **Create Telegram Source**: provide a name, a slug, and the following required configurations:
|
||||
- **Bot username**: The username of your Telegram bot (e.g., `authentik_bot`).
|
||||
- **Bot token**: The token of your Telegram bot.
|
||||
- **Request access to send messages from your bot**: enable this to allow your bot to send messages to authentik users utilizing the Telegram source for authentication.
|
||||
|
||||
3. Click **Save**.
|
||||
|
||||
:::note
|
||||
For instructions on how to display the new source on the authentik login page, refer to the [Add sources to default login page documentation](../../index.md#add-sources-to-default-login-page).
|
||||
:::
|
||||
|
||||
## Telegram source property mappings
|
||||
|
||||
[Property mappings](../../property-mappings/index.md) can be used to map Telegram user properties to authentik user properties.
|
||||
|
||||
### Expression data
|
||||
|
||||
Telegram user data is accessible to Telegram source property mappings as a dictionary named `info`.
|
||||
The dictionary contains the following fields:
|
||||
|
||||
- `id` - Telegram user ID
|
||||
- `username` - Username of the user. Might not be present.
|
||||
- `first_name` - First name of the user. Might not be present.
|
||||
- `last_name` - Last name of the user. Might not be present.
|
||||
- `photo_url` - URL of the user's profile photo. Might not be present.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Telegram Documentation - BotFather](https://core.telegram.org/bots/features#botfather)
|
||||
Reference in New Issue
Block a user