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:
		
							
								
								
									
										2
									
								
								.github/workflows/tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -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") | ||||||
| @ -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 | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -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 | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										99
									
								
								swagger.yaml
									
									
									
									
									
								
							
							
						
						
									
										99
									
								
								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 |           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 | ||||||
|  | |||||||
| @ -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
	
	Block a user
	 Jens Langhammer
					Jens Langhammer