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:
commit
e91ff4566d
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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, ""),
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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)'
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
@ -47,6 +47,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
|
||||||
Instruction: WebsocketInstructionHello,
|
Instruction: WebsocketInstructionHello,
|
||||||
Args: map[string]interface{}{
|
Args: map[string]interface{}{
|
||||||
"version": pkg.VERSION,
|
"version": pkg.VERSION,
|
||||||
|
"buildHash": pkg.BUILD(),
|
||||||
"uuid": ac.instanceUUID.String(),
|
"uuid": ac.instanceUUID.String(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -101,13 +102,14 @@ func (ac *APIController) startWSHealth() {
|
||||||
Instruction: WebsocketInstructionHello,
|
Instruction: WebsocketInstructionHello,
|
||||||
Args: map[string]interface{}{
|
Args: map[string]interface{}{
|
||||||
"version": pkg.VERSION,
|
"version": pkg.VERSION,
|
||||||
|
"buildHash": pkg.BUILD(),
|
||||||
"uuid": ac.instanceUUID.String(),
|
"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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ type ProviderInstance struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserFlags struct {
|
type UserFlags struct {
|
||||||
UserInfo *models.User
|
UserInfo models.User
|
||||||
CanSearch bool
|
CanSearch bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
89
swagger.yaml
89
swagger.yaml
|
@ -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
|
|
||||||
properties:
|
|
||||||
uuid:
|
|
||||||
title: Uuid
|
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
readOnly: true
|
uniqueItems: 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:
|
|
||||||
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:
|
|
||||||
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
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
readOnly: true
|
group_obj:
|
||||||
readOnly: true
|
$ref: '#/definitions/Group'
|
||||||
NotificationTransport:
|
NotificationTransport:
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
|
|
@ -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>`;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Reference in New Issue