outposts: don't authenticate as service user for flows to set remote-ip
set outpost token as additional header and check that token (user) if they can override remote-ip Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -38,6 +38,7 @@ USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" | ||||
| USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" | ||||
| USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources" | ||||
| USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires"  # nosec | ||||
| USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" | ||||
|  | ||||
| GRAVATAR_URL = "https://secure.gravatar.com" | ||||
| DEFAULT_AVATAR = static("dist/assets/images/user_default.png") | ||||
|  | ||||
| @ -2,10 +2,12 @@ | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from django.http import HttpRequest | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" | ||||
| USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" | ||||
| OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN"  # nosec | ||||
| DEFAULT_IP = "255.255.255.255" | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def _get_client_ip_from_meta(meta: dict[str, Any]) -> str: | ||||
| @ -27,13 +29,25 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]: | ||||
|     """Get the actual remote IP when set by an outpost. Only | ||||
|     allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set | ||||
|     to outpost""" | ||||
|     if not hasattr(request, "user"): | ||||
|     from authentik.core.models import ( | ||||
|         USER_ATTRIBUTE_CAN_OVERRIDE_IP, | ||||
|         Token, | ||||
|         TokenIntents, | ||||
|     ) | ||||
|  | ||||
|     if ( | ||||
|         OUTPOST_REMOTE_IP_HEADER not in request.META | ||||
|         or OUTPOST_TOKEN_HEADER not in request.META | ||||
|     ): | ||||
|         return None | ||||
|     if not request.user.is_authenticated: | ||||
|     tokens = Token.filter_not_expired( | ||||
|         key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API | ||||
|     ) | ||||
|     if not tokens.exists(): | ||||
|         LOGGER.warning("Attempted remote-ip override without token") | ||||
|         return None | ||||
|     if OUTPOST_REMOTE_IP_HEADER not in request.META: | ||||
|         return None | ||||
|     if request.user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): | ||||
|     user = tokens.first().user | ||||
|     if user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): | ||||
|         return None | ||||
|     return request.META[OUTPOST_REMOTE_IP_HEADER] | ||||
|  | ||||
|  | ||||
| @ -28,12 +28,18 @@ from structlog.stdlib import get_logger | ||||
| from urllib3.exceptions import HTTPError | ||||
|  | ||||
| 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_CAN_OVERRIDE_IP, | ||||
|     USER_ATTRIBUTE_SA, | ||||
|     Provider, | ||||
|     Token, | ||||
|     TokenIntents, | ||||
|     User, | ||||
| ) | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.models import InheritanceForeignKey | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.lib.utils.http import USER_ATTRIBUTE_CAN_OVERRIDE_IP | ||||
| from authentik.managed.models import ManagedModel | ||||
| from authentik.outposts.controllers.k8s.utils import get_namespace | ||||
| from authentik.outposts.docker_tls import DockerInlineTLS | ||||
|  | ||||
| @ -4,10 +4,12 @@ import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/http/cookiejar" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"goauthentik.io/api" | ||||
| @ -24,6 +26,11 @@ const ( | ||||
| 	StageAccessDenied          = StageComponent("ak-stage-access-denied") | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	HeaderAuthentikRemoteIP     = "X-authentik-remote-ip" | ||||
| 	HeaderAuthentikOutpostToken = "X-authentik-outpost-token" | ||||
| ) | ||||
|  | ||||
| type FlowExecutor struct { | ||||
| 	Params  url.Values | ||||
| 	Answers map[StageComponent]string | ||||
| @ -31,6 +38,7 @@ type FlowExecutor struct { | ||||
| 	api      *api.APIClient | ||||
| 	flowSlug string | ||||
| 	log      *log.Entry | ||||
| 	token    string | ||||
| } | ||||
|  | ||||
| func NewFlowExecutor(flowSlug string, refConfig *api.Configuration) *FlowExecutor { | ||||
| @ -56,6 +64,7 @@ func NewFlowExecutor(flowSlug string, refConfig *api.Configuration) *FlowExecuto | ||||
| 		api:      apiClient, | ||||
| 		flowSlug: flowSlug, | ||||
| 		log:      l, | ||||
| 		token:    strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1], | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -69,6 +78,16 @@ type ChallengeInt interface { | ||||
| 	GetResponseErrors() map[string][]api.ErrorDetail | ||||
| } | ||||
|  | ||||
| func (fe *FlowExecutor) DelegateClientIP(a net.Addr) { | ||||
| 	host, _, err := net.SplitHostPort(a.String()) | ||||
| 	if err != nil { | ||||
| 		fe.log.WithError(err).Warning("Failed to get remote IP") | ||||
| 		return | ||||
| 	} | ||||
| 	fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, host) | ||||
| 	fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikOutpostToken, fe.token) | ||||
| } | ||||
|  | ||||
| func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) { | ||||
| 	p, _, err := fe.api.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), appSlug).Execute() | ||||
| 	if !p.Passing { | ||||
|  | ||||
| @ -34,14 +34,8 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) { | ||||
| } | ||||
|  | ||||
| func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { | ||||
| 	host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) | ||||
| 	if err != nil { | ||||
| 		pi.log.WithError(err).Warning("Failed to get remote IP") | ||||
| 		return ldap.LDAPResultOperationsError, nil | ||||
| 	} | ||||
|  | ||||
| 	fe := outpost.NewFlowExecutor(pi.flowSlug, pi.s.ac.Client.GetConfig()) | ||||
| 	fe.ApiClient().GetConfig().AddDefaultHeader("X-authentik-remote-ip", host) | ||||
| 	fe.DelegateClientIP(conn.RemoteAddr()) | ||||
| 	fe.Params.Add("goauthentik.io/outpost/ldap", "true") | ||||
|  | ||||
| 	fe.Answers[outpost.StageIdentification] = username | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer