Пример #1
0
class Application(PolicyBindingModel):
    """Every Application which uses authentik for authentication/identification/authorization
    needs an Application record. Other authentication types can subclass this Model to
    add custom fields and other properties"""

    name = models.TextField(help_text=_("Application's display Name."))
    slug = models.SlugField(help_text=_("Internal application name, used in URLs."), unique=True)
    provider = models.OneToOneField(
        "Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
    )

    meta_launch_url = models.TextField(
        default="", blank=True, validators=[DomainlessURLValidator()]
    )
    # For template applications, this can be set to /static/authentik/applications/*
    meta_icon = models.FileField(
        upload_to="application-icons/",
        default=None,
        null=True,
        max_length=500,
    )
    meta_description = models.TextField(default="", blank=True)
    meta_publisher = models.TextField(default="", blank=True)

    @property
    def get_meta_icon(self) -> Optional[str]:
        """Get the URL to the App Icon image. If the name is /static or starts with http
        it is returned as-is"""
        if not self.meta_icon:
            return None
        if "://" in self.meta_icon.name or self.meta_icon.name.startswith("/static"):
            return self.meta_icon.name
        return self.meta_icon.url

    def get_launch_url(self) -> Optional[str]:
        """Get launch URL if set, otherwise attempt to get launch URL based on provider."""
        if self.meta_launch_url:
            return self.meta_launch_url
        if provider := self.get_provider():
            return provider.launch_url
        return None
Пример #2
0
class LDAPSource(Source):
    """Federate LDAP Directory with authentik, or create new accounts in LDAP."""

    server_uri = models.TextField(
        validators=[DomainlessURLValidator(schemes=["ldap", "ldaps"])],
        verbose_name=_("Server URI"),
    )
    bind_cn = models.TextField(verbose_name=_("Bind CN"))
    bind_password = models.TextField()
    start_tls = models.BooleanField(default=False,
                                    verbose_name=_("Enable Start TLS"))

    base_dn = models.TextField(verbose_name=_("Base DN"))
    additional_user_dn = models.TextField(
        help_text=_("Prepended to Base DN for User-queries."),
        verbose_name=_("Addition User DN"),
        blank=True,
    )
    additional_group_dn = models.TextField(
        help_text=_("Prepended to Base DN for Group-queries."),
        verbose_name=_("Addition Group DN"),
        blank=True,
    )

    user_object_filter = models.TextField(
        default="(objectClass=person)",
        help_text=_("Consider Objects matching this filter to be Users."),
    )
    group_membership_field = models.TextField(
        default="member",
        help_text=_("Field which contains members of a group."))
    group_object_filter = models.TextField(
        default="(objectClass=group)",
        help_text=_("Consider Objects matching this filter to be Groups."),
    )
    object_uniqueness_field = models.TextField(
        default="objectSid",
        help_text=_("Field which contains a unique Identifier."))

    property_mappings_group = models.ManyToManyField(
        PropertyMapping,
        default=None,
        blank=True,
        help_text=_("Property mappings used for group creation/updating."),
    )

    sync_users = models.BooleanField(default=True)
    sync_users_password = models.BooleanField(
        default=True,
        help_text=_(
            ("When a user changes their password, sync it back to LDAP. "
             "This can only be enabled on a single LDAP source.")),
        unique=True,
    )
    sync_groups = models.BooleanField(default=True)
    sync_parent_group = models.ForeignKey(Group,
                                          blank=True,
                                          null=True,
                                          default=None,
                                          on_delete=models.SET_DEFAULT)

    @property
    def component(self) -> str:
        return "ak-source-ldap-form"

    @property
    def serializer(self) -> Type[Serializer]:
        from authentik.sources.ldap.api import LDAPSourceSerializer

        return LDAPSourceSerializer

    _connection: Optional[Connection] = None

    @property
    def connection(self) -> Connection:
        """Get a fully connected and bound LDAP Connection"""
        if not self._connection:
            server = Server(self.server_uri, get_info=ALL)
            self._connection = Connection(
                server,
                raise_exceptions=True,
                user=self.bind_cn,
                password=self.bind_password,
            )

            self._connection.bind()
            if self.start_tls:
                self._connection.start_tls()
        return self._connection

    class Meta:

        verbose_name = _("LDAP Source")
        verbose_name_plural = _("LDAP Sources")
Пример #3
0
class ProxyProvider(OutpostModel, OAuth2Provider):
    """Protect applications that don't support any of the other
    Protocols by using a Reverse-Proxy."""

    internal_host = models.TextField(
        validators=[DomainlessURLValidator(schemes=("http", "https"))],
        blank=True,
    )
    external_host = models.TextField(
        validators=[DomainlessURLValidator(schemes=("http", "https"))])
    internal_host_ssl_validation = models.BooleanField(
        default=True,
        help_text=_("Validate SSL Certificates of upstream servers"),
        verbose_name=_("Internal host SSL Validation"),
    )
    mode = models.TextField(
        default=ProxyMode.PROXY,
        choices=ProxyMode.choices,
        help_text=_(
            "Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
            "internal_host."),
    )

    skip_path_regex = models.TextField(
        default="",
        blank=True,
        help_text=_(
            ("Regular expressions for which authentication is not required. "
             "Each new line is interpreted as a new Regular Expression.")),
    )

    basic_auth_enabled = models.BooleanField(
        default=False,
        verbose_name=_("Set HTTP-Basic Authentication"),
        help_text=
        _("Set a custom HTTP-Basic Authentication header based on values from authentik."
          ),
    )
    basic_auth_user_attribute = models.TextField(
        blank=True,
        verbose_name=_("HTTP-Basic Username Key"),
        help_text=_((
            "User/Group Attribute used for the user part of the HTTP-Basic Header. "
            "If not set, the user's Email address is used.")),
    )
    basic_auth_password_attribute = models.TextField(
        blank=True,
        verbose_name=_("HTTP-Basic Password Key"),
        help_text=_((
            "User/Group Attribute used for the password part of the HTTP-Basic Header."
        )),
    )

    certificate = models.ForeignKey(
        CertificateKeyPair,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )

    cookie_secret = models.TextField(default=get_cookie_secret)
    cookie_domain = models.TextField(default="", blank=True)

    @property
    def component(self) -> str:
        return "ak-provider-proxy-form"

    @property
    def serializer(self) -> type[Serializer]:
        from authentik.providers.proxy.api import ProxyProviderSerializer

        return ProxyProviderSerializer

    @property
    def launch_url(self) -> Optional[str]:
        """Use external_host as launch URL"""
        return self.external_host

    def set_oauth_defaults(self):
        """Ensure all OAuth2-related settings are correct"""
        self.client_type = ClientTypes.CONFIDENTIAL
        self.signing_key = None
        scopes = ScopeMapping.objects.filter(scope_name__in=[
            SCOPE_OPENID,
            SCOPE_OPENID_PROFILE,
            SCOPE_OPENID_EMAIL,
            SCOPE_AK_PROXY,
        ])
        self.property_mappings.add(*list(scopes))
        self.redirect_uris = _get_callback_url(self.external_host)

    def __str__(self):
        return f"Proxy Provider {self.name}"

    def get_required_objects(self) -> Iterable[models.Model | str]:
        required_models = [self]
        if self.certificate is not None:
            required_models.append(self.certificate)
        return required_models

    class Meta:

        verbose_name = _("Proxy Provider")
        verbose_name_plural = _("Proxy Providers")
        authentik_used_by_shadows = [
            "authentik_providers_oauth2.oauth2provider"
        ]
Пример #4
0
class NotificationTransport(models.Model):
    """Action which is executed when a Rule matches"""

    uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)

    name = models.TextField(unique=True)
    mode = models.TextField(choices=TransportMode.choices)

    webhook_url = models.TextField(blank=True,
                                   validators=[DomainlessURLValidator()])
    webhook_mapping = models.ForeignKey("NotificationWebhookMapping",
                                        on_delete=models.SET_DEFAULT,
                                        null=True,
                                        default=None)
    send_once = models.BooleanField(
        default=False,
        help_text=
        _("Only send notification once, for example when sending a webhook into a chat channel."
          ),
    )

    def send(self, notification: "Notification") -> list[str]:
        """Send notification to user, called from async task"""
        if self.mode == TransportMode.WEBHOOK:
            return self.send_webhook(notification)
        if self.mode == TransportMode.WEBHOOK_SLACK:
            return self.send_webhook_slack(notification)
        if self.mode == TransportMode.EMAIL:
            return self.send_email(notification)
        raise ValueError(f"Invalid mode {self.mode} set")

    def send_webhook(self, notification: "Notification") -> list[str]:
        """Send notification to generic webhook"""
        default_body = {
            "body": notification.body,
            "severity": notification.severity,
            "user_email": notification.user.email,
            "user_username": notification.user.username,
        }
        if self.webhook_mapping:
            default_body = self.webhook_mapping.evaluate(
                user=notification.user,
                request=None,
                notification=notification,
            )
        try:
            response = get_http_session().post(
                self.webhook_url,
                json=default_body,
            )
            response.raise_for_status()
        except RequestException as exc:
            raise NotificationTransportError(exc.response.text) from exc
        return [
            response.status_code,
            response.text,
        ]

    def send_webhook_slack(self, notification: "Notification") -> list[str]:
        """Send notification to slack or slack-compatible endpoints"""
        fields = [
            {
                "title": _("Severity"),
                "value": notification.severity,
                "short": True,
            },
            {
                "title": _("Dispatched for user"),
                "value": str(notification.user),
                "short": True,
            },
        ]
        if notification.event:
            for key, value in notification.event.context.items():
                if not isinstance(value, str):
                    continue
                # https://birdie0.github.io/discord-webhooks-guide/other/field_limits.html
                if len(fields) >= 25:
                    continue
                fields.append({"title": key[:256], "value": value[:1024]})
        body = {
            "username":
            "******",
            "icon_url":
            "https://goauthentik.io/img/icon.png",
            "attachments": [{
                "author_name": "authentik",
                "author_link": "https://goauthentik.io",
                "author_icon": "https://goauthentik.io/img/icon.png",
                "title": notification.body,
                "color": "#fd4b2d",
                "fields": fields,
                "footer": f"authentik v{__version__}",
            }],
        }
        if notification.event:
            body["attachments"][0]["title"] = notification.event.action
        try:
            response = get_http_session().post(self.webhook_url, json=body)
            response.raise_for_status()
        except RequestException as exc:
            text = exc.response.text if exc.response else str(exc)
            raise NotificationTransportError(text) from exc
        return [
            response.status_code,
            response.text,
        ]

    def send_email(self, notification: "Notification") -> list[str]:
        """Send notification via global email configuration"""
        subject = "authentik Notification: "
        key_value = {}
        if notification.event:
            subject += notification.event.action
            for key, value in notification.event.context.items():
                if not isinstance(value, str):
                    continue
                key_value[key] = value
        else:
            subject += notification.body[:75]
        mail = TemplateEmailMessage(
            subject=subject,
            template_name="email/generic.html",
            to=[notification.user.email],
            template_context={
                "title": subject,
                "body": notification.body,
                "key_value": key_value,
            },
        )
        # Email is sent directly here, as the call to send() should have been from a task.
        try:
            from authentik.stages.email.tasks import send_mail

            # pyright: reportGeneralTypeIssues=false
            return send_mail(mail.__dict__)  # pylint: disable=no-value-for-parameter
        except (SMTPException, ConnectionError, OSError) as exc:
            raise NotificationTransportError from exc

    def __str__(self) -> str:
        return f"Notification Transport {self.name}"

    class Meta:

        verbose_name = _("Notification Transport")
        verbose_name_plural = _("Notification Transports")