Applications proxy

L’orchestrateur d’identité Maverics peut être utilisé en tant que proxy HTTP pour protéger les applications. Cela peut s’avérer utile pour les applications utilisant l’identité via des en-têtes HTTP, tout comme pour les applications qui authentifient les utilisateurs de manière native au moyen d’autres mécanismes.

Options de configuration

Name

name est un identifiant unique pour l’application.

Type

type représente le type d’application. Lors de la définition des applications proxy, le type doit être proxy.

Acheminement de motifs

routePatterns constituent la liste des motifs qui seront utilisés pour faire correspondre une requête à l’application appropriée. L’orchestrateur établit une correspondance entre une requête donnée et le motif le plus spécifique. La correspondance des motifs est insensible à la casse.

Un motif peut être un chemin d’accès racine tel que /index.html ou encore un sous-arborescence racine telle que /dashboard/ (notez la barre oblique finale). Étant donné qu’un motif se terminant par une barre oblique désigne une sous-arborescence racine, le motif / correspond à tous les chemins d’accès non couverts par d’autres motifs enregistrés, et pas seulement à l’URL ayant un chemin d’accès contenant le caractère /.

Les motifs peuvent éventuellement commencer par un nom d’hôte, ce qui limite les correspondances aux URL de cet hôte. Les motifs spécifiques à l’hôte ont la priorité sur les motifs généraux. Par exemple, un motif de type app.example.com correspondrait à toutes les requêtes contenant l’hôte spécifié, comme https://app.example.com/dashboard.

Un motif peut être spécifié en incluant à la fois un nom d’hôte et un chemin d’accès. Par exemple, un motif de type example.com/app/ ne correspondrait qu’aux requêtes qui contiennent à la fois l’hôte et le chemin spécifiés, tels que https://example.com/app/reports.

ℹ️
Si vous obtenez une réponse HTTP 404 (introuvable) qui ne provient pas d’une application en amont, il est probable que la requête ne corresponde à aucun des motifs enregistrés.

Amont

upstream correspond à l’URL de l’application mandatée.

Préservation de l’hôte

preserveHost est un indicateur utilisé pour déterminer si l’en-tête « Host » doit être conservé pour les requêtes sortantes. Par défaut, l’orchestrateur définit l’en-tête pour qu’il corresponde à l’hôte en amont.

Transport Layer Security

tls définit une option de configuration TLS pour les requêtes sortantes vers l’application en amont. La valeur doit faire référence à un objet TLS défini dans la configuration TLS de premier niveau. Ce champ est généralement utilisé lorsque la cible en amont utilise des certificats signés par une autorité de certification dont la fiabilité fait défaut.

Page non autorisée

unauthorizedPage est l’URL vers laquelle l’utilisateur sera redirigé lorsque l’évaluation de la politique conduit à un refus d’accès à l’application.

Fournisseurs d’attributs

attrProviders définit une liste de systèmes d’accès ou de magasins de données à partir desquels sont chargés les attributs nécessaires à l’évaluation des politiques et à la construction des en-têtes.

Les fournisseurs d’attributs ne doivent être définis qu’en cas de nécessité de chargement des attributs après l’authentification. De nombreux fournisseurs d’identité renverront un ensemble complet d’attributs dans le cadre du processus d’authentification via le jeton d’identification OIDC ou la réponse SAML.

Nom

name fait référence au nom du fournisseur qui sera utilisé pour le chargement des attributs.

Mappage des noms d’utilisateur

usernameMapping définit l’attribut à utiliser en tant que clé de recherche afin de charger les attributs à partir du fournisseur d’attributs. La valeur provient généralement du fournisseur d’identité utilisé pour l’authentification primaire, par exemple {{ azure.mail }}. Pour charger un attribut du cache de session, utilisez la syntaxe {{ namespace.value }} .

En-têtes

headers constituent la liste des en-têtes HTTP qui seront ajoutés aux requêtes adressées aux applications en amont. Les en-têtes ne seront ajoutés à la requête que si l’emplacement est protégé.

Name

name correspond au nom de l’en-tête HTTP.

Valeur

value correspond à la valeur de l’en-tête HTTP. La valeur peut être une chaîne ou un attribut dynamique chargé depuis la session. Pour charger un attribut du cache de session, utilisez la syntaxe {{ namespace.value }}.

Extension de services CreateHeader

createHeaderSE est une extension de services optionnelle utilisée pour créer un en-tête HTTP personnalisé. Cette extension est souvent utilisée lorsqu’un attribut doit être enrichi ou concaténé avec des données supplémentaires.

Politiques

policies définit une liste de conditions qui déterminent si une requête donnée doit être autorisée, définissant en fin de compte les utilisateurs autorisés à accéder à l’application. Si aucune politique n’est définie, toutes les requêtes seront refusées.

Location

location correspond à un chemin URL utilisé pour faire correspondre les ressources de l’application à une politique. Une requête sera traitée en fonction de l’emplacement le plus spécifique, les emplacements basés sur des expressions régulières (regex) étant prioritaires. L’ordre de configuration des emplacements de politique sans expression régulière n’a pas d’importance, mais les emplacements qui emploient des expressions régulières conservent l’ordre dans lequel ils ont été définis.

La correspondance entre les emplacements des politiques simples n’est pas sensible à la casse. Si vous définissez l’emplacement /EXAMPLE, les requêtes présentant un chemin d’accès de type /example ou /Example seront prises en compte. Si vous vous souhaitez que la politique soit sensible à la casse, utilisez une expression régulière.

Pour appliquer la correspondance des expressions régulières à un emplacement, ajoutez ~ avant le motif (notez l’espace à la fin). Par exemple :

- location: ~ \.(jpg|png|ico|svg|ttf|js|css|gif)$

Vous pouvez utiliser des outils tels que regex101 (sélectionnez Golang) pour tester votre regex contre les chemins URL que vous aimeriez faire correspondre.

⚠️
Des expressions régulières mal construites peuvent affecter les performances de l’orchestrateur. Utilisez des ancres (^ ou $) si possible, et des compteurs ou des plages (.{0,15}) au lieu de métacaractères « gourmands » (.*).

Utilisation de la correspondance des paramètres de requête

useQueryParamsMatching est une configuration facultative utilisée pour faire correspondre les requêtes avec des paramètres de requête à la politique appropriée. La correspondance des paramètres de requête ne fonctionne qu’avec les emplacements qui utilisent des expressions régulières (c’est-à-dire précédés de ~).

Afin de faire correspondre les URL qui incluent des paramètres de requête tels que l’URL /dashboard?sort=asc, vous devez définir un emplacement similaire à : location: ~ ^/dashboard\?sort=asc.

Authentification

authentification définit la politique d’authentification pour l’emplacement.

Autorisation d’accès non authentifié

allowUnauthenticated peut être utilisé pour permettre un accès libre à une ressource. Si la valeur est « true », la ressource correspondant à cette politique ne nécessite pas d’authentification. Cet opérateur est généralement utilisé pour des ressources telles que les pages d’erreur ou de connexion.

IDPs

idps contient une liste d’un ou de plusieurs noms de fournisseurs d’identité à utiliser pour l’authentification.

Extension de services IsAuthenticated

isAuthenticatedSE est une extension de services optionnelle qui peut être utilisée pour modifier le comportement par défaut qui détermine si un utilisateur est déjà authentifié. Cette extension doit être utilisée avec authenticateSE.

Extension de services Authenticate

authenticateSE est une extension de services optionnelle utilisée pour contrôler la manière dont l’authentification sera effectuée. Cette extension doit être utilisée avec isAuthenticatedSE.

Authorization

authorization définit les règles de contrôle d’accès nécessaires à l’accès à un ensemble d’emplacements.

Tout autoriser

allowAll permet un accès libre à un emplacement donné. Cette option est généralement utilisée lorsque lorsque l’autorisation à grain fin n’est pas appliquée.

Règles

rules définit une liste de conditions de contrôle d’accès. Toutes les règles doivent être évaluées avec une valeur « true » pour permettre à l’utilisateur d’accéder à un emplacement donné.

And

and définit une liste de conditions qui doivent toutes être remplies.

Or

or définit une liste de conditions dont au moins une doit être remplie.

Opérateurs

Les opérateurs définis ci-dessous peuvent être répertoriés sous une règle or ou and. Tous les opérateurs attendent exactement deux valeurs (opérandes).

Pour charger un attribut du cache de session qui sera exploité dans la politque, utilisez la syntaxe {{ connectorName.attributeName }}. Par exemple, {{ ldap.memberOf }} pourrait être utilisé pour rédiger une politique basée sur l’appartenance à un groupe LDAP.

En outre, une méthode de requête HTTP peut être prise en compte lors de l’évaluation de la politique en utilisant la syntaxe {{ http.request.method }}. La méthode de requête HTTP sera renvoyée sous la forme d’une chaîne en majuscules, par exemple, POST. Veuillez noter que seul l’attribut de méthode de requête HTTP peut être utilisé dans la politique, la prise en charge d’autres attributs HTTP sera introduite progressivement.

equals s’évalue avec une valeur « true » si les deux valeurs sont équivalentes.

notEquals s’évalue avec une valeur « true » si les deux valeurs ne sont pas équivalentes.

contains s’évalue avec une valeur « true » si l’opérande de gauche (la chaîne complète) contient l’opérande de droite (une sous-chaîne).

notContains s’évalue avec une valeur « true » si l’opérande de gauche (la chaîne complète) ne contient pas l’opérande de droite (une sous-chaîne).

Extension de services IsAuthorized

isAuthorizedSE est une extension de services optionnelle qui peut être utilisée pour remplacer le comportement par défaut qui détermine si un utilisateur est autorisé.

En-têtes

headers définit les en-têtes HTTP pour un emplacement de politique donné. Les en-têtes définis au niveau de la politique remplaceront les en-têtes du même nom qui sont définis au niveau de l’application.

Déconnexion

logout définit les paramètres de déconnexion facultatifs pour l’application. Lors de la déconnexion d’une application, l’utilisateur est déconnecté de tous les fournisseurs d’identité définis dans la politique de l’application avec laquelle il s’est authentifié. De plus, toutes les données de session mises en cache liées à l’application seront supprimées.

Le terminal de déconnexion spécifique à l’application présente des différences importantes par rapport au terminal de déconnexion unique (SLO, Single Logout). Plus précisément, le point de terminaison SLO déconnecte l’utilisateur de tous les fournisseurs d’identité authentifiés, tandis que la déconnexion spécifique à une application ne déconnecte l’utilisateur que des fournisseurs d’identité définis dans les politiques de l’application en question.

Si un fournisseur d’identité est partagé entre plusieurs applications, l’utilisateur sera déconnecté du fournisseur d’identité partagé. Cependant, Maverics maintient la session pour les applications qui n’ont pas été déconnectées. En d’autres termes, l’utilisateur ne doit pas s’authentifier à nouveau pour les applications qui n’on pas été déconnectées formellement.

URL de déconnexion

logoutURL correspond au terminal qui sera exposé pour faciliter la déconnexion des utilisateurs de l’application.

URL de redirection post-déconnexion

postLogoutRedirectURL correspond à l’adresse vers laquelle l’utilisateur sera redirigé après une déconnexion réussie.

Extension de services LoadAttributes

loadAttrsSE est une extension de services optionnelle utilisée pour personnaliser la manière dont le chargement des attributs est effectué. Cette extension est souvent utilisée pour charger des attributs à partir de sources de données propriétaires telles que les API d’entreprise.

Extension de services ModifyRequest

modifyRequestSE est une extension de services optionnelle qui peut être utilisée pour modifier chaque requête passant par l’application.

Extension de services ModifyResponse

modifyResponseSE est une extension de services optionnelle qui peut être utilisée pour modifier chaque réponse passant par l’application.

Connexion en amont

upstreamLogin est une configuration facultative utilisée pour déterminer si une requête adressée à une application en amont est authentifiée et pour pouvoir se connecter à une application en amont. De telles situations sont courantes lorsqu’une application gère ses propres sessions ou s’authentifie directement auprès d’un magasin de données comme LDAP ou une base de données relationnelle.

Extension de services IsLoggedIn

isLoggedInSE permet de déterminer si une application en amont a fait l’objet d’une connexion.

Extension de services Login

loginSE permet de se connecter à une application en amont.

Extension de services Handle Unauthorized

handleUnauthorizedSE est une extension de services optionnelle qui peut être utilisée pour remplacer le comportement par défaut lorsqu’une évaluation de la politique refuse l’accès à l’application.

Exemple

Configuration standard

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      - app.example.com
      - www.example.com/app/
    upstream: https://app-internal.example.com
    tls: exampleAppTLS
    unauthorizedPage: https://app.example.com/unauthorized
    attrProviders:
      - name: ldap
        usernameMapping: "{{ azure.emailaddress }}"
    headers:
      - name: SM_USER
        value: "{{ azure.emailaddress }}"
      - name: groups
        value: "{{ ldap.memberOf }}"
      - name: Organization
        value: Example Inc.
    policies:
      - location: /
        authentication:
          allowUnauthenticated: false
          idps:
            - azure
        authorization:
          allowAll: false
          rules:
            - and:
                - equals: ["{{ ldap.department }}", "Engineering"]
                - notEquals: ["{{ ldap.title }}", "Junior Software Engineer"]
                - contains: ["{{ ldap.memberOf }}", "cn=admins,ou=groups,ou=example,ou=com"]
            - or:
                - contains: ["{{ azure.emailaddress }}", "@example.com"]
                - contains: ["{{ azure.emailaddress }}", "@example.io"]
            - or:
                - equals: ["{{ http.request.method }}", "GET"]
                - equals: ["{{ http.request.method }}", "OPTIONS"]
      - location: /index.html
        authentication:
          allowUnauthenticated: false
          idps:
            - azure
        authorization:
          allowAll: true
        headers:
          - name: firstName
            value: "{{ ldap.givenname }}"
          - name: lastName
            value: "{{ ldap.sn }}"
      - location: ~ \.(jpg|png|ico|svg|ttf|js|css|gif)$
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true
    logout:
      logoutURL: app.example.com/logout
      postLogoutRedirectURL: /login

Extensions de services Auth

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      - app.example.com
    upstream: https://app-internal.example.com
    policies:
      - location: /
        authentication:
          isAuthenticatedSE:
            funcName: IsAuthenticated
            file: /etc/maverics/auth.go
          authenticateSE:
            funcName: Authenticate
            file: /etc/maverics/auth.go
        authorization:
          isAuthorizedSE:
            funcName: IsAuthorized
            file: /etc/maverics/auth.go

/etc/maverics/auth.go

package main

import (
	"net/http"
	"strings"

	"github.com/strata-io/service-extension/orchestrator"
)

func IsAuthenticated(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) bool {
	logger := api.Logger()
	logger.Debug("se", "determining if user is authenticated")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return false
	}

	isAzureAuth, err := session.GetString("azure.authenticated")
	if err != nil {
		logger.Error("se", "unable to retrieve session value 'azure.authenticated'", "error", err.Error())
		return false
	}
	if isAzureAuth == "true" {
		return true
	}

	return false
}

func Authenticate(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) {
	logger := api.Logger()
	logger.Debug("se", "authenticating user")

	azureIDP, err := api.IdentityProvider("azure")
	if err != nil {
		logger.Error(
			"se", "failed to retrieve Azure IDP",
			"error", err.Error(),
		)
		http.Error(
			rw,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError,
		)
		return
	}
	azureIDP.Login(rw, req)
}

func IsAuthorized(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) bool {
	logger := api.Logger()
	logger.Debug("se", "determining if user is authorized")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return false
	}

	rawGroups, _ := session.GetString("azure.groups")
	groups := strings.Split(rawGroups, ",")
	for _, group := range groups {
		if group == "executives" {
			return true
        }
    }

	return false
}

Extension de services Header Creation

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      - app.example.com
    upstream: https://app-internal.example.com
    headers:
      - name: firstName
        value: "{{ azure.givenname }}"
      - name: lastName
        value: "{{ azure.surname }}"
      - createHeaderSE:
          funcName: CreateHeader
          file: /etc/maverics/header.go
    policies:
      - location: /
        authentication:
          idps:
            - azure
        authorization:
          allowAll: true

/etc/maverics/header.go

package main

import (
	"fmt"
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func CreateHeader(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) (http.Header, error) {
	logger := api.Logger()
	logger.Debug("se", "building custom header")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return nil, err
	}

	firstName, err := session.GetString("azure.givenname")
	if err != nil {
        return nil, fmt.Errorf("unable to retrieve attribute 'azure.givenname': %w", err)
	}
	surname, err := session.GetString("azure.surname")
	if err != nil {
		return nil, fmt.Errorf("unable to retrieve attribute 'azure.givenname': %w", err)
	}
	preferredName := fmt.Sprintf(`%s 'The Great' %s`, firstName, surname)
	
	return http.Header{"preferredName": []string{preferredName}}, nil
}

Extension de services Attribute Loading

Cette extension est utilisée pour charger des informations concernant l’utilisateur authentifié. En règle générale, les informations relatives aux attributs sont stockées dans la session, ce qui permet de les utiliser pour prendre des décisions en matière de politique ou pour les utiliser dans les en-têtes transmis à la ressource protégée.

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    loadAttrsSE:
      funcName: LoadAttributes
      file: /etc/maverics/auth.go
    headers:
      - name: SM_USER
        value: "{{ azure.mail }}"
      - name: firstName
        value: "{{ ldap.givenname }}"
      - name: lastName
        value: "{{ ldap.sn }}"
      - name: mobile
        value: "{{ ldap.mobile }}"
    policies:
      - location: /
        authentication:
          idps:
            - azure
        authorization:
          allowAll: true

/etc/maverics/auth.go

package main

import (
	"fmt"
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func LoadAttributes(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) error {
	logger := api.Logger()
	logger.Debug("se", "loading custom attributes from LDAP")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return err
	}

	mail, err := session.GetString("azure.email")
	if err != nil {
		return fmt.Errorf("failed to find user email required for LDAP query: %w", err)
	}

	ldap, err := api.AttributeProvider("ldap")
	if err != nil {
		return fmt.Errorf("failed to find LDAP attribute provider")
	}

	attrs, err := ldap.Query(mail, []string{"givenname", "sn", "mobile"})
	if err != nil {
		return fmt.Errorf("failed to query LDAP: %w", err)
	}

	for k, v := range attrs {
		logger.Debug(
			"se", "setting LDAP attribute on session",
			"attribute", k,
			"value", v,
		)
		_ = session.SetString(k, v)
	}

	err = session.Save()
	if err != nil {
		return fmt.Errorf("unable to save session state: %w", err)
	}
	return nil
}

Extension de services Request and Response Modification

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    modifyRequestSE:
      funcName: ModifyRequest
      file: /etc/maverics/proxy.go
    modifyResponseSE:
      funcName: ModifyResponse
      file: /etc/maverics/proxy.go
    policies:
      - location: /
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true

/etc/maverics/proxy.go

package main

import (
	"net/http"
	"time"

	"github.com/strata-io/service-extension/orchestrator"
)

func ModifyRequest(api orchestrator.Orchestrator, req *http.Request) {
	api.Logger().Debug("se", "setting header on request")
	req.Header.Set("X-Req-Time", time.Now().String())
}

func ModifyResponse(api orchestrator.Orchestrator, resp *http.Response) {
	if resp.Request.URL.Path != "/special/path/" {
		return
	}

	api.Logger().Debug("se", "setting status code for '/special/path/' resource")
	resp.StatusCode = http.StatusOK
}

Extensions de services Upstream Application Login

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    upstreamLogin:
      isLoggedInSE:
        funcName: IsLoggedIn
        file: /etc/maverics/login.go
      loginSE:
        funcName: Login
        file: /etc/maverics/login.go
    policies:
      - location: /
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true
      - location: ~ \.(jpg|png|ico|svg|ttf|js|css|gif)$
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true

/etc/maverics/login.go

package main

import (
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func IsLoggedIn(api orchestrator.Orchestrator, _ http.ResponseWriter, req *http.Request) bool {
	logger := api.Logger()
	logger.Debug("se", "determining if user has session with upstream app")

	if req.URL.Path == "/login" {
		return true
	}
	
	_, err := req.Cookie(".ASPXAUTH")
	if err != nil {
		logger.Debug("se", "user is not authenticated with upstream app")
		return false
	}

	logger.Debug("se", "user is authenticated with upstream app")
	return true
}

func Login(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) error {
	api.Logger().Info("se", "redirecting user to upstream app's login page")
	
	http.Redirect(rw, req, "/login", http.StatusFound)
	return nil
}

Extension de services Handle Unauthorized

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    handleUnauthorizedSE:
      funcName: HandleUnauthorized
      file: /etc/maverics/unauthorized.go
    policies:
      - location: /
        authentication:
          idps:
            - azure
        authorization:
          allowAll: true
      - location: /reports
        authentication:
          idps:
            - azure
        authorization:
          rules:
            - or:
                - equals: ["{{ azure.title }}", "CEO"]
                - equals: ["{{ azure.title }}", "CFO"]
                - equals: ["{{ azure.title }}", "COO"]

/etc/maverics/unauthorized.go

package main

import (
	"fmt"
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func HandleUnauthorized(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) {
    api.Logger().Debug("se", "handling unauthorized request")

    http.Error(
        rw,
        fmt.Sprintf(
            "Access denied to %s. Please contact [email protected] for help.",
            req.URL.Path,
        ),
        http.StatusUnauthorized,
    )
}