Merge branch 'next' into version-2021.5

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	outpost/pkg/version.go
This commit is contained in:
Jens Langhammer 2021-05-12 20:49:58 +02:00
commit e91ff4566d
25 changed files with 112 additions and 127 deletions

View File

@ -27,7 +27,7 @@ jobs:
-f Dockerfile . -f Dockerfile .
docker-compose up --no-start docker-compose up --no-start
docker-compose start postgresql redis docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
- name: Extract version number - name: Extract version number
id: get_version id: get_version
uses: actions/github-script@0.2.0 uses: actions/github-script@0.2.0

View File

@ -2,22 +2,25 @@
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupSerializer
from authentik.events.models import NotificationRule from authentik.events.models import NotificationRule
class NotificationRuleSerializer(ModelSerializer): class NotificationRuleSerializer(ModelSerializer):
"""NotificationRule Serializer""" """NotificationRule Serializer"""
group_obj = GroupSerializer(read_only=True, source="group")
class Meta: class Meta:
model = NotificationRule model = NotificationRule
depth = 2
fields = [ fields = [
"pk", "pk",
"name", "name",
"transports", "transports",
"severity", "severity",
"group", "group",
"group_obj",
] ]

View File

@ -42,6 +42,7 @@ outposts:
# Placeholders: # Placeholders:
# %(type)s: Outpost type; proxy, ldap, etc # %(type)s: Outpost type; proxy, ldap, etc
# %(version)s: Current version; 2021.4.1 # %(version)s: Current version; 2021.4.1
# %(build_hash)s: Build hash if you're running a beta version
docker_image_base: "beryju/authentik-%(type)s:%(version)s" docker_image_base: "beryju/authentik-%(type)s:%(version)s"
authentik: authentik:

View File

@ -18,8 +18,6 @@ class OutpostSerializer(ModelSerializer):
"""Outpost Serializer""" """Outpost Serializer"""
config = JSONField(validators=[is_dict], source="_config") config = JSONField(validators=[is_dict], source="_config")
# TODO: Remove _config again, this is only here for legacy with older outposts
_config = JSONField(validators=[is_dict], read_only=True)
providers_obj = ProviderSerializer(source="providers", many=True, read_only=True) providers_obj = ProviderSerializer(source="providers", many=True, read_only=True)
def validate_config(self, config) -> dict: def validate_config(self, config) -> dict:
@ -42,7 +40,6 @@ class OutpostSerializer(ModelSerializer):
"service_connection", "service_connection",
"token_identifier", "token_identifier",
"config", "config",
"_config",
] ]

View File

@ -82,6 +82,7 @@ class OutpostConsumer(AuthJsonConsumer):
) )
if msg.instruction == WebsocketMessageInstruction.HELLO: if msg.instruction == WebsocketMessageInstruction.HELLO:
state.version = msg.args.get("version", None) state.version = msg.args.get("version", None)
state.build_hash = msg.args.get("buildHash", "")
elif msg.instruction == WebsocketMessageInstruction.ACK: elif msg.instruction == WebsocketMessageInstruction.ACK:
return return
state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5) state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5)

View File

@ -1,11 +1,12 @@
"""Base Controller""" """Base Controller"""
from dataclasses import dataclass from dataclasses import dataclass
from os import environ
from typing import Optional from typing import Optional
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from structlog.testing import capture_logs from structlog.testing import capture_logs
from authentik import __version__ from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import Outpost, OutpostServiceConnection from authentik.outposts.models import Outpost, OutpostServiceConnection
@ -69,4 +70,8 @@ class BaseController:
def get_container_image(self) -> str: def get_container_image(self) -> str:
"""Get container image to use for this outpost""" """Get container image to use for this outpost"""
image_name_template: str = CONFIG.y("outposts.docker_image_base") image_name_template: str = CONFIG.y("outposts.docker_image_base")
return image_name_template % {"type": self.outpost.type, "version": __version__} return image_name_template % {
"type": self.outpost.type,
"version": __version__,
"build_hash": environ.get(ENV_GIT_HASH_KEY, ""),
}

View File

@ -1,6 +1,7 @@
"""Outpost models""" """Outpost models"""
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
from os import environ
from typing import Iterable, Optional, Union from typing import Iterable, Optional, Union
from uuid import uuid4 from uuid import uuid4
@ -26,7 +27,7 @@ from packaging.version import LegacyVersion, Version, parse
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from urllib3.exceptions import HTTPError from urllib3.exceptions import HTTPError
from authentik import __version__ from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.core.models import USER_ATTRIBUTE_SA, Provider, Token, TokenIntents, User from authentik.core.models import USER_ATTRIBUTE_SA, Provider, Token, TokenIntents, User
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
@ -411,6 +412,7 @@ class OutpostState:
last_seen: Optional[datetime] = field(default=None) last_seen: Optional[datetime] = field(default=None)
version: Optional[str] = field(default=None) version: Optional[str] = field(default=None)
version_should: Union[Version, LegacyVersion] = field(default=OUR_VERSION) version_should: Union[Version, LegacyVersion] = field(default=OUR_VERSION)
build_hash: str = field(default="")
_outpost: Optional[Outpost] = field(default=None) _outpost: Optional[Outpost] = field(default=None)
@ -419,6 +421,8 @@ class OutpostState:
"""Check if outpost version matches our version""" """Check if outpost version matches our version"""
if not self.version: if not self.version:
return False return False
if self.build_hash != environ.get(ENV_GIT_HASH_KEY, ""):
return False
return parse(self.version) < OUR_VERSION return parse(self.version) < OUR_VERSION
@staticmethod @staticmethod

View File

@ -320,6 +320,7 @@ CELERY_RESULT_BACKEND = (
# Database backup # Database backup
DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage"
DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"} DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"}
DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql"
if CONFIG.y("postgresql.s3_backup"): if CONFIG.y("postgresql.s3_backup"):
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
DBBACKUP_STORAGE_OPTIONS = { DBBACKUP_STORAGE_OPTIONS = {

View File

@ -116,7 +116,10 @@ stages:
command: 'buildAndPush' command: 'buildAndPush'
Dockerfile: 'outpost/proxy.Dockerfile' Dockerfile: 'outpost/proxy.Dockerfile'
buildContext: 'outpost/' buildContext: 'outpost/'
tags: "gh-$(branchName)" tags: |
gh-$(branchName)
gh-$(Build.SourceVersion)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'
- job: ldap_build_docker - job: ldap_build_docker
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
@ -141,4 +144,7 @@ stages:
command: 'buildAndPush' command: 'buildAndPush'
Dockerfile: 'outpost/ldap.Dockerfile' Dockerfile: 'outpost/ldap.Dockerfile'
buildContext: 'outpost/' buildContext: 'outpost/'
tags: "gh-$(branchName)" tags: |
gh-$(branchName)
gh-$(Build.SourceVersion)
arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)'

View File

@ -1,4 +1,6 @@
FROM golang:1.16.4 AS builder FROM golang:1.16.4 AS builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
WORKDIR /work WORKDIR /work

View File

@ -1,7 +1,6 @@
package ak package ak
import ( import (
"fmt"
"math/rand" "math/rand"
"net/url" "net/url"
"os" "os"
@ -43,7 +42,7 @@ type APIController struct {
// NewAPIController initialise new API Controller instance from URL and API token // NewAPIController initialise new API Controller instance from URL and API token
func NewAPIController(akURL url.URL, token string) *APIController { func NewAPIController(akURL url.URL, token string) *APIController {
transport := httptransport.New(akURL.Host, client.DefaultBasePath, []string{akURL.Scheme}) transport := httptransport.New(akURL.Host, client.DefaultBasePath, []string{akURL.Scheme})
transport.Transport = SetUserAgent(getTLSTransport(), fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)) transport.Transport = SetUserAgent(getTLSTransport(), pkg.UserAgent())
// create the transport // create the transport
auth := httptransport.BearerToken(token) auth := httptransport.BearerToken(token)

View File

@ -23,7 +23,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
header := http.Header{ header := http.Header{
"Authorization": []string{authHeader}, "Authorization": []string{authHeader},
"User-Agent": []string{fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)}, "User-Agent": []string{pkg.UserAgent()},
} }
value, set := os.LookupEnv("AUTHENTIK_INSECURE") value, set := os.LookupEnv("AUTHENTIK_INSECURE")
@ -46,8 +46,9 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
msg := websocketMessage{ msg := websocketMessage{
Instruction: WebsocketInstructionHello, Instruction: WebsocketInstructionHello,
Args: map[string]interface{}{ Args: map[string]interface{}{
"version": pkg.VERSION, "version": pkg.VERSION,
"uuid": ac.instanceUUID.String(), "buildHash": pkg.BUILD(),
"uuid": ac.instanceUUID.String(),
}, },
} }
err := ws.WriteJSON(msg) err := ws.WriteJSON(msg)
@ -76,7 +77,7 @@ func (ac *APIController) startWSHandler() {
var wsMsg websocketMessage var wsMsg websocketMessage
err := ac.wsConn.ReadJSON(&wsMsg) err := ac.wsConn.ReadJSON(&wsMsg)
if err != nil { if err != nil {
logger.Println("read:", err) logger.WithError(err).Warning("ws write error, reconnecting")
ac.wsConn.CloseAndReconnect() ac.wsConn.CloseAndReconnect()
continue continue
} }
@ -100,14 +101,15 @@ func (ac *APIController) startWSHealth() {
aliveMsg := websocketMessage{ aliveMsg := websocketMessage{
Instruction: WebsocketInstructionHello, Instruction: WebsocketInstructionHello,
Args: map[string]interface{}{ Args: map[string]interface{}{
"version": pkg.VERSION, "version": pkg.VERSION,
"uuid": ac.instanceUUID.String(), "buildHash": pkg.BUILD(),
"uuid": ac.instanceUUID.String(),
}, },
} }
err := ac.wsConn.WriteJSON(aliveMsg) err := ac.wsConn.WriteJSON(aliveMsg)
ac.logger.WithField("loop", "ws-health").Trace("hello'd") ac.logger.WithField("loop", "ws-health").Trace("hello'd")
if err != nil { if err != nil {
ac.logger.WithField("loop", "ws-health").Println("write:", err) ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error, reconnecting")
ac.wsConn.CloseAndReconnect() ac.wsConn.CloseAndReconnect()
continue continue
} }

View File

@ -33,7 +33,7 @@ func doGlobalSetup(config map[string]interface{}) {
default: default:
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
} }
log.WithField("version", pkg.VERSION).Info("Starting authentik outpost") log.WithField("buildHash", pkg.BUILD()).WithField("version", pkg.VERSION).Info("Starting authentik outpost")
var dsn string var dsn string
if config[ConfigErrorReportingEnabled].(bool) { if config[ConfigErrorReportingEnabled].(bool) {

View File

@ -2,20 +2,22 @@ package ldap
import ( import (
"net" "net"
"strings"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
) )
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
ls.log.WithField("boundDN", bindDN).Info("bind") ls.log.WithField("bindDN", bindDN).Info("bind")
bindDN = strings.ToLower(bindDN)
for _, instance := range ls.providers { for _, instance := range ls.providers {
username, err := instance.getUsername(bindDN) username, err := instance.getUsername(bindDN)
if err == nil { if err == nil {
return instance.Bind(username, bindPW, conn) return instance.Bind(username, bindDN, bindPW, conn)
} else { } else {
ls.log.WithError(err).Debug("Username not for instance") ls.log.WithError(err).Debug("Username not for instance")
} }
} }
ls.log.WithField("boundDN", bindDN).WithField("request", "bind").Warning("No provider found for request") ls.log.WithField("bindDN", bindDN).WithField("request", "bind").Warning("No provider found for request")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }

View File

@ -47,7 +47,7 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
return "", errors.New("failed to find cn") return "", errors.New("failed to find cn")
} }
func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
pi.log.WithError(err).Warning("Failed to create cookiejar") pi.log.WithError(err).Warning("Failed to create cookiejar")
@ -67,9 +67,9 @@ func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn)
} }
params := url.Values{} params := url.Values{}
params.Add("goauthentik.io/outpost/ldap", "true") params.Add("goauthentik.io/outpost/ldap", "true")
passed, err := pi.solveFlowChallenge(username, bindPW, client, params.Encode()) passed, err := pi.solveFlowChallenge(username, bindPW, client, params.Encode(), 1)
if err != nil { if err != nil {
pi.log.WithField("boundDN", username).WithError(err).Warning("failed to solve challenge") pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to solve challenge")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
if !passed { if !passed {
@ -82,25 +82,25 @@ func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn)
}, httptransport.PassThroughAuth) }, httptransport.PassThroughAuth)
if err != nil { if err != nil {
if _, denied := err.(*core.CoreApplicationsCheckAccessForbidden); denied { if _, denied := err.(*core.CoreApplicationsCheckAccessForbidden); denied {
pi.log.WithField("boundDN", username).Info("Access denied for user") pi.log.WithField("bindDN", bindDN).Info("Access denied for user")
return ldap.LDAPResultInsufficientAccessRights, nil return ldap.LDAPResultInsufficientAccessRights, nil
} }
pi.log.WithField("boundDN", username).WithError(err).Warning("failed to check access") pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to check access")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
pi.log.WithField("boundDN", username).Info("User has access") pi.log.WithField("bindDN", bindDN).Info("User has access")
// Get user info to store in context // Get user info to store in context
userInfo, err := pi.s.ac.Client.Core.CoreUsersMe(&core.CoreUsersMeParams{ userInfo, err := pi.s.ac.Client.Core.CoreUsersMe(&core.CoreUsersMeParams{
Context: context.Background(), Context: context.Background(),
HTTPClient: client, HTTPClient: client,
}, httptransport.PassThroughAuth) }, httptransport.PassThroughAuth)
if err != nil { if err != nil {
pi.log.WithField("boundDN", username).WithError(err).Warning("failed to get user info") pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to get user info")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
pi.boundUsersMutex.Lock() pi.boundUsersMutex.Lock()
pi.boundUsers[username] = UserFlags{ pi.boundUsers[bindDN] = UserFlags{
UserInfo: userInfo.Payload.User, UserInfo: *userInfo.Payload.User,
CanSearch: pi.SearchAccessCheck(userInfo.Payload.User), CanSearch: pi.SearchAccessCheck(userInfo.Payload.User),
} }
defer pi.boundUsersMutex.Unlock() defer pi.boundUsersMutex.Unlock()
@ -112,7 +112,8 @@ func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn)
func (pi *ProviderInstance) SearchAccessCheck(user *models.User) bool { func (pi *ProviderInstance) SearchAccessCheck(user *models.User) bool {
for _, group := range user.Groups { for _, group := range user.Groups {
for _, allowedGroup := range pi.searchAllowedGroups { for _, allowedGroup := range pi.searchAllowedGroups {
if &group.Pk == allowedGroup { pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
if group.Pk.String() == allowedGroup.String() {
pi.log.WithField("group", group.Name).Info("Allowed access to search") pi.log.WithField("group", group.Name).Info("Allowed access to search")
return true return true
} }
@ -139,7 +140,7 @@ func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {
}() }()
} }
func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client, urlParams string) (bool, error) { func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client, urlParams string, depth int) (bool, error) {
challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{ challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{
FlowSlug: pi.flowSlug, FlowSlug: pi.flowSlug,
Query: urlParams, Query: urlParams,
@ -169,6 +170,10 @@ func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, c
} }
response, err := pi.s.ac.Client.Flows.FlowsExecutorSolve(responseParams, pi.s.ac.Auth) response, err := pi.s.ac.Client.Flows.FlowsExecutorSolve(responseParams, pi.s.ac.Auth)
pi.log.WithField("component", response.Payload.Component).WithField("type", *response.Payload.Type).Debug("Got response") pi.log.WithField("component", response.Payload.Component).WithField("type", *response.Payload.Type).Debug("Got response")
switch response.Payload.Component {
case "ak-stage-access-denied":
return false, errors.New("got ak-stage-access-denied")
}
if *response.Payload.Type == "redirect" { if *response.Payload.Type == "redirect" {
return true, nil return true, nil
} }
@ -184,5 +189,8 @@ func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, c
} }
} }
} }
return pi.solveFlowChallenge(bindDN, password, client, urlParams) if depth >= 10 {
return false, errors.New("exceeded stage recursion depth")
}
return pi.solveFlowChallenge(bindDN, password, client, urlParams, depth+1)
} }

View File

@ -29,10 +29,13 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest,
pi.boundUsersMutex.RLock() pi.boundUsersMutex.RLock()
defer pi.boundUsersMutex.RUnlock() defer pi.boundUsersMutex.RUnlock()
flags, ok := pi.boundUsers[bindDN] flags, ok := pi.boundUsers[bindDN]
pi.log.WithField("bindDN", bindDN).WithField("ok", ok).Debugf("%+v\n", flags)
if !ok { if !ok {
pi.log.Debug("User info not cached")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
} }
if !flags.CanSearch { if !flags.CanSearch {
pi.log.Debug("User can't search")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
} }

View File

@ -31,7 +31,7 @@ type ProviderInstance struct {
} }
type UserFlags struct { type UserFlags struct {
UserInfo *models.User UserInfo models.User
CanSearch bool CanSearch bool
} }

View File

@ -8,8 +8,8 @@ import (
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
) )
func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
ls.log.WithField("boundDN", boundDN).WithField("baseDN", searchReq.BaseDN).Info("search") ls.log.WithField("bindDN", bindDN).WithField("baseDN", searchReq.BaseDN).Info("search")
if searchReq.BaseDN == "" { if searchReq.BaseDN == "" {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil
} }
@ -21,7 +21,7 @@ func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn
for _, provider := range ls.providers { for _, provider := range ls.providers {
providerBase, _ := goldap.ParseDN(provider.BaseDN) providerBase, _ := goldap.ParseDN(provider.BaseDN)
if providerBase.AncestorOf(bd) { if providerBase.AncestorOf(bd) {
return provider.Search(boundDN, searchReq, conn) return provider.Search(bindDN, searchReq, conn)
} }
} }
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request")

View File

@ -1,3 +1,16 @@
package pkg package pkg
import (
"fmt"
"os"
)
const VERSION = "2021.5.1-rc8" const VERSION = "2021.5.1-rc8"
func BUILD() string {
return os.Getenv("GIT_BUILD_HASH")
}
func UserAgent() string {
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
}

View File

@ -1,4 +1,6 @@
FROM golang:1.16.4 AS builder FROM golang:1.16.4 AS builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
WORKDIR /work WORKDIR /work

View File

@ -15694,6 +15694,7 @@ definitions:
NotificationRule: NotificationRule:
required: required:
- name - name
- transports
type: object type: object
properties: properties:
pk: pk:
@ -15706,38 +15707,17 @@ definitions:
type: string type: string
minLength: 1 minLength: 1
transports: transports:
description: Select which transports should be used to notify the user. If
none are selected, the notification will only be shown in the authentik
UI.
type: array type: array
items: items:
required: description: Select which transports should be used to notify the user.
- name If none are selected, the notification will only be shown in the authentik
- mode UI.
type: object type: string
properties: format: uuid
uuid: uniqueItems: true
title: Uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
mode:
title: Mode
type: string
enum:
- webhook
- webhook_slack
- email
webhook_url:
title: Webhook url
type: string
send_once:
title: Send once
description: Only send notification once, for example when sending a
webhook into a chat channel.
type: boolean
readOnly: true
severity: severity:
title: Severity title: Severity
description: Controls which severity level the created notifications will description: Controls which severity level the created notifications will
@ -15748,57 +15728,14 @@ definitions:
- warning - warning
- alert - alert
group: group:
required: title: Group
- name description: Define which group of users this notification should be sent
type: object and shown to. If left empty, Notification won't ben sent.
properties: type: string
group_uuid: format: uuid
title: Group uuid x-nullable: true
type: string group_obj:
format: uuid $ref: '#/definitions/Group'
readOnly: true
name:
title: Name
type: string
maxLength: 80
minLength: 1
is_superuser:
title: Is superuser
description: Users added to this group will be superusers.
type: boolean
attributes:
title: Attributes
type: object
parent:
required:
- name
- parent
type: object
properties:
group_uuid:
title: Group uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
maxLength: 80
minLength: 1
is_superuser:
title: Is superuser
description: Users added to this group will be superusers.
type: boolean
attributes:
title: Attributes
type: object
parent:
title: Parent
type: string
format: uuid
x-nullable: true
readOnly: true
readOnly: true
NotificationTransport: NotificationTransport:
required: required:
- name - name

View File

@ -67,7 +67,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
<option value="" ?selected=${this.instance?.group === undefined}>---------</option> <option value="" ?selected=${this.instance?.group === undefined}>---------</option>
${until(new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then(groups => { ${until(new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then(groups => {
return groups.results.map(group => { return groups.results.map(group => {
return html`<option value=${ifDefined(group.pk)} ?selected=${this.instance?.group?.groupUuid === group.pk}>${group.name}</option>`; return html`<option value=${ifDefined(group.pk)} ?selected=${this.instance?.group === group.pk}>${group.name}</option>`;
}); });
}), html`<option>${t`Loading...`}</option>`)} }), html`<option>${t`Loading...`}</option>`)}
</select> </select>
@ -80,7 +80,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
${until(new EventsApi(DEFAULT_CONFIG).eventsTransportsList({}).then(transports => { ${until(new EventsApi(DEFAULT_CONFIG).eventsTransportsList({}).then(transports => {
return transports.results.map(transport => { return transports.results.map(transport => {
const selected = Array.from(this.instance?.transports || []).some(su => { const selected = Array.from(this.instance?.transports || []).some(su => {
return su.uuid == transport.pk; return su == transport.pk;
}); });
return html`<option value=${ifDefined(transport.pk)} ?selected=${selected}>${transport.name}</option>`; return html`<option value=${ifDefined(transport.pk)} ?selected=${selected}>${transport.name}</option>`;
}); });

View File

@ -55,7 +55,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
return [ return [
html`${item.name}`, html`${item.name}`,
html`${item.severity}`, html`${item.severity}`,
html`${item.group?.name || t`None (rule disabled)`}`, html`${item.groupObj?.name || t`None (rule disabled)`}`,
html` html`
<ak-forms-modal> <ak-forms-modal>
<span slot="submit"> <span slot="submit">

View File

@ -42,13 +42,12 @@ export class OutpostHealthElement extends LitElement {
return html`<ak-spinner></ak-spinner>`; return html`<ak-spinner></ak-spinner>`;
} }
if (this.outpostHealth.length === 0) { if (this.outpostHealth.length === 0) {
return html`<li> return html`
<ul> <ul>
<li role="cell"> <li role="cell">
<ak-label color=${PFColor.Grey} text=${t`Not available`}></ak-label> <ak-label color=${PFColor.Grey} text=${t`Not available`}></ak-label>
</li> </li>
</ul> </ul>`;
</li>`;
} }
return html`<ul>${this.outpostHealth.map((h) => { return html`<ul>${this.outpostHealth.map((h) => {
return html`<li> return html`<li>

View File

@ -70,7 +70,7 @@ export class AuthenticatorValidateStageForm extends ModelForm<AuthenticatorValid
<ak-form-element-horizontal <ak-form-element-horizontal
label=${t`Not configured action`} label=${t`Not configured action`}
?required=${true} ?required=${true}
name="mode"> name="notConfiguredAction">
<select class="pf-c-form-control" @change=${(ev: Event) => { <select class="pf-c-form-control" @change=${(ev: Event) => {
const target = ev.target as HTMLSelectElement; const target = ev.target as HTMLSelectElement;
if (target.selectedOptions[0].value === AuthenticatorValidateStageNotConfiguredActionEnum.Configure) { if (target.selectedOptions[0].value === AuthenticatorValidateStageNotConfiguredActionEnum.Configure) {