Compare commits
6 Commits
v2024.5.17
...
v2024.5.21
Author | SHA1 | Date | |
---|---|---|---|
db095331e8 | |||
781bfcab19 | |||
6d0a3826ce | |||
28ef57b305 | |||
8b1c649af0 | |||
5a34d5917f |
@ -23,6 +23,7 @@ RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
|
||||
# Patch config
|
||||
RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
&& yq -i '.layers.queue.templateDir = "/usr/share/bouncer/layers/queue/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
&& yq -i '.layers.authn.templateDir = "/usr/share/bouncer/layers/authn/templates"' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
&& yq -i '.admin.auth.privateKey = "/etc/bouncer/admin-key.json"' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
&& yq -i '.redis.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
&& yq -i '.redis.writeTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
|
@ -4,4 +4,3 @@ Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entit
|
||||
|
||||
- [Authn (`authn-*`)](./authn/README.md) - Authentification des accès (SSO)
|
||||
- [Queue](./queue.md) - File d'attente dynamique
|
||||
- [Circuit Breaker](./circuitbreaker.md) - Coupure d'accès à un site ou une sous section de celui ci
|
||||
|
@ -7,6 +7,8 @@ Les informations liées à l'utilisateur authentifié sont ensuite injectables d
|
||||
## Layers
|
||||
|
||||
- [`authn-oidc`](./oidc.md) - Authentification OpenID Connect
|
||||
- [`authn-network`](./network.md) - Authentification par origine d'accès réseau
|
||||
- [`authn-basic`](./basic.md) - Authentification "Basic Auth"
|
||||
|
||||
## Schéma des options
|
||||
|
||||
@ -14,9 +16,9 @@ En plus de leurs options spécifiques tous les layers `authn-*` partagent un cer
|
||||
|
||||
Voir le [schéma](../../../../../internal/proxy/director/layer/authn/layer-options.json).
|
||||
|
||||
## Règles d'injection d'entêtes
|
||||
## Moteur de règles
|
||||
|
||||
L'option `headers.rules` permet de définir une liste de règles utilisant un DSL définissant de manière dynamique quels entêtes seront injectés dans la requête transitant par le layer.
|
||||
L'option `rules` permet de définir une liste de règles utilisant un DSL définissant de manière dynamique quels entêtes seront injectés dans la requête transitant par le layer. Les règles permettent également d'interdire l'accès à un utilisateur via la fonction `forbidden()` (voir section "Fonctions").
|
||||
|
||||
La liste des instructions est exécutée séquentiellement.
|
||||
|
||||
@ -30,6 +32,10 @@ Le comportement des règles par défaut est le suivant:
|
||||
|
||||
### Fonctions
|
||||
|
||||
#### `forbidden()`
|
||||
|
||||
Interdire l'accès à l'utilisateur.
|
||||
|
||||
#### `set_header(name string, value string)`
|
||||
|
||||
Définir la valeur d'une entête HTTP via son nom `name` et sa valeur `value`.
|
||||
|
50
doc/fr/references/layers/authn/basic.md
Normal file
50
doc/fr/references/layers/authn/basic.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Layer `authn-basic`
|
||||
|
||||
## Description
|
||||
|
||||
Ce layer permet d'ajouter une authentification de type [`Basic Auth`](https://en.wikipedia.org/wiki/Basic_access_authentication) au service distant.
|
||||
|
||||
## Type
|
||||
|
||||
`authn-basic`
|
||||
|
||||
## Schéma des options
|
||||
|
||||
Les options disponibles pour le layer sont décrites via un [schéma JSON](https://json-schema.org/specification). Elles sont documentées dans le [schéma visible ici](../../../../../internal/proxy/director/layer/authn/basic/layer-options.json).
|
||||
|
||||
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
|
||||
|
||||
## Objet `user` et attributs
|
||||
|
||||
L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
|
||||
|
||||
- `user.subject` sera initialisé avec le nom d'utilisateur identifié ;
|
||||
- `user.attrs` sera composé des attributs associés à l'utilisation (voir les options).
|
||||
|
||||
## Métriques
|
||||
|
||||
Les [métriques Prometheus](../../metrics.md) suivantes sont exposées par ce layer.
|
||||
|
||||
### `bouncer_layer_authn_basic_forbidden_total{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total de tentatives d'accès bloquées
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_authn_basic_forbidden_total Bouncer's authn-basic layer total forbidden accesses
|
||||
# TYPE bouncer_layer_authn_basic_forbidden_total counter
|
||||
bouncer_layer_authn_basic_forbidden_total{layer="basic",proxy="dummy"} 1
|
||||
```
|
||||
|
||||
### `bouncer_layer_authn_basic_authorized_total{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total de tentatives d'accès autorisées
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_authn_basic_authorized_total Bouncer's authn-basic layer total authorized accesses
|
||||
# TYPE bouncer_layer_authn_basic_authorized_total counter
|
||||
bouncer_layer_authn_basic_authorized_total{layer="basic",proxy="dummy"} 2
|
||||
```
|
50
doc/fr/references/layers/authn/network.md
Normal file
50
doc/fr/references/layers/authn/network.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Layer `authn-network`
|
||||
|
||||
## Description
|
||||
|
||||
Ce layer permet d'ajouter une authentification par origine réseau au service distant.
|
||||
|
||||
## Type
|
||||
|
||||
`authn-network`
|
||||
|
||||
## Schéma des options
|
||||
|
||||
Les options disponibles pour le layer sont décrites via un [schéma JSON](https://json-schema.org/specification). Elles sont documentées dans le [schéma visible ici](../../../../../internal/proxy/director/layer/authn/network/layer-options.json).
|
||||
|
||||
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
|
||||
|
||||
## Objet `user` et attributs
|
||||
|
||||
L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
|
||||
|
||||
- `user.subject` sera initialisé avec le couple `<remote_address>:<remote_port>` ;
|
||||
- `user.attrs` sera vide.
|
||||
|
||||
## Métriques
|
||||
|
||||
Les [métriques Prometheus](../../metrics.md) suivantes sont exposées par ce layer.
|
||||
|
||||
### `bouncer_layer_authn_network_forbidden_total{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total de tentatives d'accès bloquées
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_authn_network_forbidden_total Bouncer's authn-network layer total forbbiden accesses
|
||||
# TYPE bouncer_layer_authn_network_forbidden_total counter
|
||||
bouncer_layer_authn_network_forbidden_total{layer="network",proxy="dummy"} 1
|
||||
```
|
||||
|
||||
### `bouncer_layer_authn_network_authorized_total{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total de tentatives d'accès autorisées
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_authn_network_authorized_total Bouncer's authn-network layer total authorized accesses
|
||||
# TYPE bouncer_layer_authn_network_authorized_total counter
|
||||
bouncer_layer_authn_network_authorized_total{layer="network",proxy="dummy"} 2
|
||||
```
|
@ -1,41 +0,0 @@
|
||||
# Layer "Circuit Breaker"
|
||||
|
||||
## Description
|
||||
|
||||
Ce layer permet de bloquer l'accès à un site (ou une section de celui ci) ciblé par un proxy.
|
||||
|
||||
## Type
|
||||
|
||||
`circuitbreaker`
|
||||
|
||||
## Options
|
||||
|
||||
### `authorizedCIDRs`
|
||||
|
||||
- **Type:** `[]string`
|
||||
- **Valeur par défaut:** `[]`
|
||||
- **Description:** Autoriser les adresses distantes contenues dans un des masques réseau (en notation ["CIDR"](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation) définis à contourner la restriction d'accès.
|
||||
|
||||
### `matchURLs`
|
||||
|
||||
- **Type:** `[]string`
|
||||
- **Valeur par défaut:** `["*"]`
|
||||
- **Description:** Limiter l'action du layer à cette liste de patrons d'URLs.
|
||||
|
||||
Par exemple, si vous souhaitez limiter votre restriction d'accès à l'ensemble d'une section "`/blog`" d'un site, vous pouvez déclarer la valeur `["*/blog*"]`. Les autres URLs du site ne seront pas affectées par la restriction.
|
||||
|
||||
### `templateBlock`
|
||||
|
||||
- **Type:** `string`
|
||||
- **Valeur par défaut:** `"default"`
|
||||
- **Description:** Bloc du template HTML pour effectuer le rendu de la page indiquant la restriction d'accès.
|
||||
|
||||
Voir le [fichier de configuration de référence](../../../../misc/packaging/common/config.yml), section `layers.circuitbreaker` pour voir les options permettant de personnaliser le chemin du répertoire contenant les templates.
|
||||
|
||||
## Schéma
|
||||
|
||||
Voir le [schéma JSON](../../../../internal/proxy/director/layer/circuitbreaker/layer-options.json).
|
||||
|
||||
## Métriques
|
||||
|
||||
_Aucune [métrique Prometheus](../metrics.md) n'est exportée par ce layer._
|
@ -79,3 +79,4 @@ Par défaut ce serveur écoute sur le port 8082. Il est possible de modifier l'a
|
||||
## Ressources
|
||||
|
||||
- [Référence du layer `authn-oidc`](../../fr/references/layers/authn/oidc.md)
|
||||
- [Moteur de règles](../../fr//references/layers/authn/README.md)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Il est possible d'amorcer des données par défaut (i.e. des "proxies" et "layers" associés) via la configuration du serveur d'administration.
|
||||
|
||||
> **Attention** Ce mécanisme de modifiera pas des proxies déjà existants dans la base de données du serveur Bouncer. Autrement dit, si un proxy est déjà pré-existant lors du démarrage du serveur Bouncer, il ne sera pas modifié.
|
||||
> **Attention** Par défaut ce mécanisme de modifiera pas des proxies déjà existants dans la base de données du serveur Bouncer. Autrement dit, si un proxy est déjà pré-existant lors du démarrage du serveur Bouncer, il ne sera pas modifié. Vous pouvez utiliser l'attribut `recreate: true` pour modifier ce comportement.
|
||||
|
||||
La définition des proxies et layers par défaut s'effectue dans la section `bootstrap` du fichier de configuration. Deux possibilités pour définir les proxys à charger par défaut:
|
||||
|
||||
@ -35,6 +35,7 @@ bootstrap:
|
||||
proxies:
|
||||
# my-proxy:
|
||||
# enabled: true # Activer/désactiver le proxy
|
||||
# recreate: false # Forcer ou non la recréation du proxy même si celui existe
|
||||
# from: ["*"] # Filtre d'origine d'activation du proxy
|
||||
# to: "https://example.net" # Destination du proxy
|
||||
# weight: 0 # Priorité du proxy
|
||||
|
@ -4,7 +4,7 @@ import "time"
|
||||
|
||||
type LayersConfig struct {
|
||||
Queue QueueLayerConfig `yaml:"queue"`
|
||||
CircuitBreaker CircuitBreakerLayerConfig `yaml:"circuitbreaker"`
|
||||
Authn AuthnLayerConfig `yaml:"authn"`
|
||||
}
|
||||
|
||||
func NewDefaultLayersConfig() LayersConfig {
|
||||
@ -13,8 +13,8 @@ func NewDefaultLayersConfig() LayersConfig {
|
||||
TemplateDir: "./layers/queue/templates",
|
||||
DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
|
||||
},
|
||||
CircuitBreaker: CircuitBreakerLayerConfig{
|
||||
TemplateDir: "./layers/circuitbreaker/templates",
|
||||
Authn: AuthnLayerConfig{
|
||||
TemplateDir: "./layers/authn/templates",
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,6 @@ type QueueLayerConfig struct {
|
||||
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
|
||||
}
|
||||
|
||||
type CircuitBreakerLayerConfig struct {
|
||||
type AuthnLayerConfig struct {
|
||||
TemplateDir InterpolatedString `yaml:"templateDir"`
|
||||
}
|
||||
|
75
internal/proxy/director/layer/authn/basic/authenticator.go
Normal file
75
internal/proxy/director/layer/authn/basic/authenticator.go
Normal file
@ -0,0 +1,75 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
}
|
||||
|
||||
// Authenticate implements authn.Authenticator.
|
||||
func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) {
|
||||
options, err := fromStoreOptions(layer.Options)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
username, password, ok := r.BasicAuth()
|
||||
|
||||
unauthorized := func() {
|
||||
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s", charset="UTF-8"`, stripNonASCII(options.Realm)))
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
unauthorized()
|
||||
return nil, errors.WithStack(authn.ErrSkipRequest)
|
||||
}
|
||||
|
||||
for _, userInfo := range options.Users {
|
||||
if matches := a.matchUser(userInfo, username, password); !matches {
|
||||
continue
|
||||
}
|
||||
|
||||
metricAuthorizedTotal.With(prometheus.Labels{
|
||||
metricLabelLayer: string(layer.Name),
|
||||
metricLabelProxy: string(layer.Proxy),
|
||||
}).Add(1)
|
||||
|
||||
user := authn.NewUser(userInfo.Username, userInfo.Attributes)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
metricForbiddenTotal.With(prometheus.Labels{
|
||||
metricLabelLayer: string(layer.Name),
|
||||
metricLabelProxy: string(layer.Proxy),
|
||||
}).Add(1)
|
||||
|
||||
unauthorized()
|
||||
|
||||
return nil, errors.WithStack(authn.ErrSkipRequest)
|
||||
}
|
||||
|
||||
func (a *Authenticator) matchUser(user User, username, password string) bool {
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
expectedUsernameHash := sha256.Sum256([]byte(user.Username))
|
||||
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) == nil
|
||||
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
|
||||
var (
|
||||
_ authn.Authenticator = &Authenticator{}
|
||||
)
|
40
internal/proxy/director/layer/authn/basic/layer-options.json
Normal file
40
internal/proxy/director/layer/authn/basic/layer-options.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"users": {
|
||||
"title": "Listes des comptes autorisés",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"title": "Compte autorisé à la connexion",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"title": "Nom d'utilisateur",
|
||||
"type": "string"
|
||||
},
|
||||
"passwordHash": {
|
||||
"title": "Empreinte bcrypt du mot de passe de l'utilisateur",
|
||||
"description": "Utiliser la commande 'htpasswd -BnC 10 \"\" | tr -d \":\n\"' pour générer l'empreinte",
|
||||
"type": "string"
|
||||
},
|
||||
"attributes": {
|
||||
"title": "Attributs associés à l'utilisateur",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"username",
|
||||
"passwordHash"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
12
internal/proxy/director/layer/authn/basic/layer.go
Normal file
12
internal/proxy/director/layer/authn/basic/layer.go
Normal file
@ -0,0 +1,12 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "authn-basic"
|
||||
|
||||
func NewLayer(funcs ...authn.OptionFunc) *authn.Layer {
|
||||
return authn.NewLayer(LayerType, &Authenticator{}, funcs...)
|
||||
}
|
43
internal/proxy/director/layer/authn/basic/layer_options.go
Normal file
43
internal/proxy/director/layer/authn/basic/layer_options.go
Normal file
@ -0,0 +1,43 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LayerOptions struct {
|
||||
authn.LayerOptions
|
||||
Users []User `mapstructure:"users"`
|
||||
Realm string `mapstructure:"realm"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string `mapstructure:"username"`
|
||||
PasswordHash string `mapstructure:"passwordHash"`
|
||||
Attributes map[string]any `mapstructure:"attributes"`
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||
layerOptions := LayerOptions{
|
||||
LayerOptions: authn.DefaultLayerOptions(),
|
||||
Realm: "Restricted area",
|
||||
Users: make([]User, 0),
|
||||
}
|
||||
|
||||
config := mapstructure.DecoderConfig{
|
||||
Result: &layerOptions,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(&config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := decoder.Decode(storeOptions); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &layerOptions, nil
|
||||
}
|
31
internal/proxy/director/layer/authn/basic/metrics.go
Normal file
31
internal/proxy/director/layer/authn/basic/metrics.go
Normal file
@ -0,0 +1,31 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const (
|
||||
metricNamespace = "bouncer_layer_authn_basic"
|
||||
metricLabelProxy = "proxy"
|
||||
metricLabelLayer = "layer"
|
||||
)
|
||||
|
||||
var (
|
||||
metricAuthorizedTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "authorized_total",
|
||||
Help: "Bouncer's authn-basic layer total authorized accesses",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
metricForbiddenTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "forbidden_total",
|
||||
Help: "Bouncer's authn-basic layer total forbidden accesses",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package circuitbreaker
|
||||
package basic
|
||||
|
||||
import (
|
||||
_ "embed"
|
11
internal/proxy/director/layer/authn/basic/util.go
Normal file
11
internal/proxy/director/layer/authn/basic/util.go
Normal file
@ -0,0 +1,11 @@
|
||||
package basic
|
||||
|
||||
func stripNonASCII(s string) string {
|
||||
rs := make([]rune, 0, len(s))
|
||||
for _, r := range s {
|
||||
if r <= 127 {
|
||||
rs = append(rs, r)
|
||||
}
|
||||
}
|
||||
return string(rs)
|
||||
}
|
@ -12,25 +12,36 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"title": "Options de configuration du mécanisme d'injection d'entêtes HTTP liés à l'authentification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rules": {
|
||||
"title": "Liste des règles définissant les actions d'injection/réécriture d'entêtes HTTP",
|
||||
"title": "Liste des règles définissant les actions à appliquer sur la requête",
|
||||
"description": "Voir la documentation (ficher 'doc/fr/references/layers/authn/README.md', section 'Règles d'injection d'entêtes') pour plus d'informations sur le fonctionnement des règles",
|
||||
"type": "array",
|
||||
"default": [
|
||||
"del_headers('Remote-*')",
|
||||
"set_header('Remote-User', user.subject)",
|
||||
"map( toPairs(user.attrs), { let name = replace(lower(string(get(#, 0))), '_', '-'); set_header('Remote-User-Attr-' + name, get(#, 1)) })"
|
||||
"map(toPairs(user.attrs), { let name = replace(lower(string(get(#, 0))), '_', '-'); set_header('Remote-User-Attr-' + name, get(#, 1)) })"
|
||||
],
|
||||
"item": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"templates": {
|
||||
"title": "Options de configuration des templates utilisés en fonction de l'état de l'authentification",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"forbidden": {
|
||||
"title": "Options de configuration de rendu de la page affichée en cas d'accès interdit (HTTP 403 Forbidden)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"block": {
|
||||
"title": "Nom du bloc au sein du template à exécuter",
|
||||
"description": "Voir fichier 'layers/authn/templates/forbidden.gohtml'",
|
||||
"type": "string",
|
||||
"default": "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
@ -14,6 +17,8 @@ import (
|
||||
type Layer struct {
|
||||
layerType store.LayerType
|
||||
auth Authenticator
|
||||
|
||||
templateDir string
|
||||
}
|
||||
|
||||
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
@ -55,14 +60,24 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrForbidden) {
|
||||
l.renderForbiddenPage(w, r, layer, options, user)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := l.injectHeaders(r, options, user); err != nil {
|
||||
logger.Error(ctx, "could not inject headers", logger.E(errors.WithStack(err)))
|
||||
if err := l.applyRules(r, options, user); err != nil {
|
||||
if errors.Is(err, ErrForbidden) {
|
||||
l.renderForbiddenPage(w, r, layer, options, user)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not apply rules", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
@ -74,6 +89,11 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrForbidden) {
|
||||
l.renderForbiddenPage(w, r, layer, options, user)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
@ -88,15 +108,58 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Layer) renderForbiddenPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions, user *User) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
l.renderPage(w, r, layer, "forbidden", options.Templates.Forbidden.Block, user)
|
||||
}
|
||||
|
||||
func (l *Layer) renderPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, page string, block string, user *User) {
|
||||
ctx := r.Context()
|
||||
|
||||
pattern := filepath.Join(l.templateDir, page+".gohtml")
|
||||
|
||||
logger.Info(ctx, "loading authn templates", logger.F("pattern", pattern))
|
||||
|
||||
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not load authn templates", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
templateData := struct {
|
||||
Layer *store.Layer
|
||||
User *User
|
||||
}{
|
||||
Layer: layer,
|
||||
User: user,
|
||||
}
|
||||
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, block, templateData); err != nil {
|
||||
logger.Error(ctx, "could not render authn page", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer
|
||||
func (l *Layer) LayerType() store.LayerType {
|
||||
return l.layerType
|
||||
}
|
||||
|
||||
func NewLayer(layerType store.LayerType, auth Authenticator) *Layer {
|
||||
func NewLayer(layerType store.LayerType, auth Authenticator, funcs ...OptionFunc) *Layer {
|
||||
opts := NewOptions(funcs...)
|
||||
|
||||
return &Layer{
|
||||
layerType: layerType,
|
||||
auth: auth,
|
||||
templateDir: opts.TemplateDir,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,17 +11,21 @@ import (
|
||||
|
||||
type LayerOptions struct {
|
||||
MatchURLs []string `mapstructure:"matchURLs"`
|
||||
Headers HeadersOptions `mapstructure:"headers"`
|
||||
Rules []string `mapstructure:"rules"`
|
||||
Templates TemplatesOptions `mapstructure:"templates"`
|
||||
}
|
||||
|
||||
type HeadersOptions struct {
|
||||
Rules []string `mapstructure:"rules"`
|
||||
type TemplatesOptions struct {
|
||||
Forbidden TemplateOptions `mapstructure:"forbidden"`
|
||||
}
|
||||
|
||||
type TemplateOptions struct {
|
||||
Block string `mapstructure:"block"`
|
||||
}
|
||||
|
||||
func DefaultLayerOptions() LayerOptions {
|
||||
return LayerOptions{
|
||||
MatchURLs: []string{"*"},
|
||||
Headers: HeadersOptions{
|
||||
Rules: []string{
|
||||
"del_headers('Remote-*')",
|
||||
"set_header('Remote-User', user.subject)",
|
||||
@ -35,8 +39,13 @@ func DefaultLayerOptions() LayerOptions {
|
||||
})
|
||||
`,
|
||||
},
|
||||
Templates: TemplatesOptions{
|
||||
Forbidden: TemplateOptions{
|
||||
Block: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||
|
83
internal/proxy/director/layer/authn/network/authenticator.go
Normal file
83
internal/proxy/director/layer/authn/network/authenticator.go
Normal file
@ -0,0 +1,83 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
}
|
||||
|
||||
// Authenticate implements authn.Authenticator.
|
||||
func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
options, err := fromStoreOptions(layer.Options)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
matches, err := a.matchAnyAuthorizedCIDRs(ctx, r.RemoteAddr, options.AuthorizedCIDRs)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user := authn.NewUser(r.RemoteAddr, map[string]any{})
|
||||
|
||||
if !matches {
|
||||
metricForbiddenTotal.With(prometheus.Labels{
|
||||
metricLabelLayer: string(layer.Name),
|
||||
metricLabelProxy: string(layer.Proxy),
|
||||
}).Add(1)
|
||||
|
||||
return user, errors.WithStack(authn.ErrForbidden)
|
||||
}
|
||||
|
||||
metricAuthorizedTotal.With(prometheus.Labels{
|
||||
metricLabelLayer: string(layer.Name),
|
||||
metricLabelProxy: string(layer.Proxy),
|
||||
}).Add(1)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) {
|
||||
remoteHost, _, err := net.SplitHostPort(remoteHostPort)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
remoteAddr := net.ParseIP(remoteHost)
|
||||
if remoteAddr == nil {
|
||||
return false, errors.Errorf("remote host '%s' is not a valid ip address", remoteHost)
|
||||
}
|
||||
|
||||
for _, rawCIDR := range CIDRs {
|
||||
_, net, err := net.ParseCIDR(rawCIDR)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
match := net.Contains(remoteAddr)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "comparing remote host with authorized cidrs", logger.F("remoteAddr", remoteAddr))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ authn.Authenticator = &Authenticator{}
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorizedCIDRs": {
|
||||
"title": "Liste des adresses réseau d'origine autorisées (au format CIDR)",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
12
internal/proxy/director/layer/authn/network/layer.go
Normal file
12
internal/proxy/director/layer/authn/network/layer.go
Normal file
@ -0,0 +1,12 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "authn-network"
|
||||
|
||||
func NewLayer(funcs ...authn.OptionFunc) *authn.Layer {
|
||||
return authn.NewLayer(LayerType, &Authenticator{}, funcs...)
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
package circuitbreaker
|
||||
package network
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LayerOptions struct {
|
||||
MatchURLs []string `mapstructure:"matchURLs"`
|
||||
authn.LayerOptions
|
||||
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
|
||||
TemplateBlock string `mapstructure:"templateBlock"`
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||
layerOptions := LayerOptions{
|
||||
MatchURLs: []string{"*"},
|
||||
LayerOptions: authn.DefaultLayerOptions(),
|
||||
AuthorizedCIDRs: []string{},
|
||||
TemplateBlock: "default",
|
||||
}
|
31
internal/proxy/director/layer/authn/network/metrics.go
Normal file
31
internal/proxy/director/layer/authn/network/metrics.go
Normal file
@ -0,0 +1,31 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const (
|
||||
metricNamespace = "bouncer_layer_authn_network"
|
||||
metricLabelProxy = "proxy"
|
||||
metricLabelLayer = "layer"
|
||||
)
|
||||
|
||||
var (
|
||||
metricAuthorizedTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "authorized_total",
|
||||
Help: "Bouncer's authn-network layer total authorized accesses",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
metricForbiddenTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "forbidden_total",
|
||||
Help: "Bouncer's authn-network layer total forbbiden accesses",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
)
|
8
internal/proxy/director/layer/authn/network/schema.go
Normal file
8
internal/proxy/director/layer/authn/network/schema.go
Normal file
@ -0,0 +1,8 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
@ -1,4 +1,4 @@
|
||||
package circuitbreaker
|
||||
package authn
|
||||
|
||||
type Options struct {
|
||||
TemplateDir string
|
||||
@ -6,10 +6,16 @@ type Options struct {
|
||||
|
||||
type OptionFunc func(*Options)
|
||||
|
||||
func defaultOptions() *Options {
|
||||
return &Options{
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
TemplateDir: "./templates",
|
||||
}
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithTemplateDir(templateDir string) OptionFunc {
|
@ -12,7 +12,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (l *Layer) getRuleOptions(r *http.Request) []expr.Option {
|
||||
func (l *Layer) getHeaderRuleOptions(r *http.Request) []expr.Option {
|
||||
options := make([]expr.Option, 0)
|
||||
|
||||
setHeader := expr.Function(
|
||||
@ -67,8 +67,8 @@ func (l *Layer) getRuleOptions(r *http.Request) []expr.Option {
|
||||
return options
|
||||
}
|
||||
|
||||
func (l *Layer) injectHeaders(r *http.Request, options *LayerOptions, user *User) error {
|
||||
rules := options.Headers.Rules
|
||||
func (l *Layer) applyRules(r *http.Request, options *LayerOptions, user *User) error {
|
||||
rules := options.Rules
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -77,16 +77,32 @@ func (l *Layer) injectHeaders(r *http.Request, options *LayerOptions, user *User
|
||||
"user": user,
|
||||
}
|
||||
|
||||
rulesOptions := l.getRuleOptions(r)
|
||||
rulesOptions := l.getHeaderRuleOptions(r)
|
||||
|
||||
var ruleErr error
|
||||
forbidden := expr.Function(
|
||||
"forbidden",
|
||||
func(params ...any) (any, error) {
|
||||
ruleErr = errors.WithStack(ErrForbidden)
|
||||
return true, nil
|
||||
},
|
||||
new(func() bool),
|
||||
)
|
||||
|
||||
rulesOptions = append(rulesOptions, forbidden)
|
||||
|
||||
for i, r := range rules {
|
||||
program, err := expr.Compile(r, rulesOptions...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not compile header rule #%d", i)
|
||||
return errors.Wrapf(err, "could not compile rule #%d", i)
|
||||
}
|
||||
|
||||
if _, err := expr.Run(program, env); err != nil {
|
||||
return errors.Wrapf(err, "could not execute header rule #%d", i)
|
||||
return errors.Wrapf(err, "could not execute rule #%d", i)
|
||||
}
|
||||
|
||||
if ruleErr != nil {
|
||||
return errors.WithStack(ruleErr)
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchURLs": {
|
||||
"title": "Liste de filtrage des URLs sur lesquelles le layer est actif",
|
||||
"description": "Par exemple, si vous souhaitez limiter votre layer à l'ensemble d'une section '`/blog`' d'un site, vous pouvez déclarer la valeur `['*/blog*']`. Les autres URLs du site ne seront pas affectées par ce layer.",
|
||||
"default": [
|
||||
"*"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"authorizedCIDRs": {
|
||||
"title": "Liste des adressages réseau d'origine autorisés (au format CIDR)",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"templateBlock": {
|
||||
"title": "Nom du bloc au sein du template de la page d'information à rendre",
|
||||
"default": "default",
|
||||
"description": "Voir fichier layers/circuitbreaker/templates/default.gohtml",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
package circuitbreaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "circuitbreaker"
|
||||
|
||||
type Layer struct {
|
||||
templateDir string
|
||||
loadOnce sync.Once
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer
|
||||
func (l *Layer) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
|
||||
// Middleware implements director.MiddlewareLayer
|
||||
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
options, err := fromStoreOptions(layer.Options)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
matches, err := l.matchAnyAuthorizedCIDRs(ctx, r.RemoteAddr, options.AuthorizedCIDRs)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not match authorized cidrs", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if matches {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
matches = wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
|
||||
if !matches {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
l.renderCircuitBreakerPage(w, r, layer, options)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Layer) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) {
|
||||
remoteHost, _, err := net.SplitHostPort(remoteHostPort)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
remoteAddr := net.ParseIP(remoteHost)
|
||||
if remoteAddr == nil {
|
||||
return false, errors.Errorf("remote host '%s' is not a valid ip address", remoteHost)
|
||||
}
|
||||
|
||||
for _, rawCIDR := range CIDRs {
|
||||
_, net, err := net.ParseCIDR(rawCIDR)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
match := net.Contains(remoteAddr)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "comparing remote host with authorized cidrs", logger.F("remoteAddr", remoteAddr))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (l *Layer) renderCircuitBreakerPage(w http.ResponseWriter, r *http.Request, layer *store.Layer, options *LayerOptions) {
|
||||
ctx := r.Context()
|
||||
|
||||
pattern := filepath.Join(l.templateDir, "*.gohtml")
|
||||
|
||||
logger.Info(ctx, "loading circuit breaker page templates", logger.F("pattern", pattern))
|
||||
|
||||
tmpl, err := template.New("").Funcs(sprig.FuncMap()).ParseGlob(pattern)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not load circuit breaker templates", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
templateData := struct {
|
||||
Layer *store.Layer
|
||||
LayerOptions *LayerOptions
|
||||
}{
|
||||
Layer: layer,
|
||||
LayerOptions: options,
|
||||
}
|
||||
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, options.TemplateBlock, templateData); err != nil {
|
||||
logger.Error(ctx, "could not render circuit breaker page", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func New(funcs ...OptionFunc) *Layer {
|
||||
opts := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return &Layer{
|
||||
templateDir: opts.TemplateDir,
|
||||
}
|
||||
}
|
||||
|
||||
var _ director.MiddlewareLayer = &Layer{}
|
27
internal/setup/authn_basic_layer.go
Normal file
27
internal/setup/authn_basic_layer.go
Normal file
@ -0,0 +1,27 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn/basic"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extended, err := schema.Extend(authn.RawLayerOptionsSchema, basic.RawLayerOptionsSchema)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not extend authn base layer options schema"))
|
||||
}
|
||||
|
||||
RegisterLayer(basic.LayerType, setupAuthnBasicLayer, extended)
|
||||
}
|
||||
|
||||
func setupAuthnBasicLayer(conf *config.Config) (director.Layer, error) {
|
||||
options := []authn.OptionFunc{
|
||||
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
|
||||
}
|
||||
|
||||
return basic.NewLayer(options...), nil
|
||||
}
|
27
internal/setup/authn_network_layer.go
Normal file
27
internal/setup/authn_network_layer.go
Normal file
@ -0,0 +1,27 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn/network"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extended, err := schema.Extend(authn.RawLayerOptionsSchema, network.RawLayerOptionsSchema)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not extend authn base layer options schema"))
|
||||
}
|
||||
|
||||
RegisterLayer(network.LayerType, setupAuthnNetworkLayer, extended)
|
||||
}
|
||||
|
||||
func setupAuthnNetworkLayer(conf *config.Config) (director.Layer, error) {
|
||||
options := []authn.OptionFunc{
|
||||
authn.WithTemplateDir(string(conf.Layers.Authn.TemplateDir)),
|
||||
}
|
||||
|
||||
return network.NewLayer(options...), nil
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/circuitbreaker"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterLayer(circuitbreaker.LayerType, setupCircuitBreakerLayer, circuitbreaker.RawLayerOptionsSchema)
|
||||
}
|
||||
|
||||
func setupCircuitBreakerLayer(conf *config.Config) (director.Layer, error) {
|
||||
options := []circuitbreaker.OptionFunc{
|
||||
circuitbreaker.WithTemplateDir(string(conf.Layers.CircuitBreaker.TemplateDir)),
|
||||
}
|
||||
|
||||
return circuitbreaker.New(
|
||||
options...,
|
||||
), nil
|
||||
}
|
102
layers/authn/templates/forbidden.gohtml
Normal file
102
layers/authn/templates/forbidden.gohtml
Normal file
@ -0,0 +1,102 @@
|
||||
{{ define "default" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Accès interdit - {{ .Layer.Name }}</title>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#card {
|
||||
padding: 1.5em 1em;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 2px 2px #cccccc1c;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 0.7em;
|
||||
margin-top: 2em;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="card">
|
||||
<h2 class="title">Accès interdit</h2>
|
||||
<p>
|
||||
La page à laquelle vous souhaitez accéder vous est actuellement
|
||||
interdite.
|
||||
</p>
|
||||
{{ if .User }}
|
||||
<p>
|
||||
<small
|
||||
><i
|
||||
>Vous êtes actuellement connu comme "<code
|
||||
>{{ .User.Subject }}</code
|
||||
>".</i
|
||||
></small
|
||||
>
|
||||
</p>
|
||||
{{ end }}
|
||||
<p class="footer">
|
||||
Propulsé par
|
||||
<a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
@ -1,73 +0,0 @@
|
||||
{{ define "default" }}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Accès bloqué - {{ .Layer.Name }}</title>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#card {
|
||||
padding: 1.5em 1em;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 2px 2px #cccccc1c;
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 0.7em;
|
||||
margin-top: 2em;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="card">
|
||||
<h2 class="title">Page indisponible</h2>
|
||||
<p>La page à laquelle vous souhaitez accéder est actuellement indisponible.</p>
|
||||
<p class="footer">Propulsé par <a href="https://forge.cadoles.com/Cadoles/bouncer">Bouncer</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
@ -172,10 +172,10 @@ layers:
|
||||
# Temps de vie par défaut d'une session
|
||||
defaultKeepAlive: 1m
|
||||
|
||||
# Configuration du layer "circuitbreaker"
|
||||
circuitbreaker:
|
||||
# Configuration du layer "authn"
|
||||
authn:
|
||||
# Répertoire contenant les templates
|
||||
templateDir: "/etc/bouncer/layers/circuitbreaker/templates"
|
||||
templateDir: "/etc/bouncer/layers/authn/templates"
|
||||
|
||||
# Configuration d'une série de proxy/layers
|
||||
# à créer par défaut par le serveur d'administration
|
||||
|
Reference in New Issue
Block a user