feat: transform circuitbreaker layer in authn-network layer
This commit is contained in:
parent
5ed194618a
commit
5a34d5917f
|
@ -23,6 +23,7 @@ RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
|
||||||
# Patch config
|
# Patch config
|
||||||
RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bouncer_linux_amd64_v1/config.yml \
|
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.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 '.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.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||||
&& yq -i '.redis.writeTimeout = "30s"' /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)
|
- [Authn (`authn-*`)](./authn/README.md) - Authentification des accès (SSO)
|
||||||
- [Queue](./queue.md) - File d'attente dynamique
|
- [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,7 @@ Les informations liées à l'utilisateur authentifié sont ensuite injectables d
|
||||||
## Layers
|
## Layers
|
||||||
|
|
||||||
- [`authn-oidc`](./oidc.md) - Authentification OpenID Connect
|
- [`authn-oidc`](./oidc.md) - Authentification OpenID Connect
|
||||||
|
- [`authn-network`](./network.md) - Authentification par origine d'accès réseau
|
||||||
|
|
||||||
## Schéma des options
|
## Schéma des options
|
||||||
|
|
||||||
|
|
|
@ -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._
|
|
2
go.mod
2
go.mod
|
@ -31,6 +31,7 @@ require (
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver v1.5.0 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
|
@ -109,6 +110,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cdr.dev/slog v1.6.1 // indirect
|
cdr.dev/slog v1.6.1 // indirect
|
||||||
|
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -15,8 +15,12 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
|
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
|
||||||
|
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||||
|
|
|
@ -4,7 +4,7 @@ import "time"
|
||||||
|
|
||||||
type LayersConfig struct {
|
type LayersConfig struct {
|
||||||
Queue QueueLayerConfig `yaml:"queue"`
|
Queue QueueLayerConfig `yaml:"queue"`
|
||||||
CircuitBreaker CircuitBreakerLayerConfig `yaml:"circuitbreaker"`
|
Authn AuthnLayerConfig `yaml:"authn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultLayersConfig() LayersConfig {
|
func NewDefaultLayersConfig() LayersConfig {
|
||||||
|
@ -13,8 +13,8 @@ func NewDefaultLayersConfig() LayersConfig {
|
||||||
TemplateDir: "./layers/queue/templates",
|
TemplateDir: "./layers/queue/templates",
|
||||||
DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
|
DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
|
||||||
},
|
},
|
||||||
CircuitBreaker: CircuitBreakerLayerConfig{
|
Authn: AuthnLayerConfig{
|
||||||
TemplateDir: "./layers/circuitbreaker/templates",
|
TemplateDir: "./layers/authn/templates",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,6 @@ type QueueLayerConfig struct {
|
||||||
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
|
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CircuitBreakerLayerConfig struct {
|
type AuthnLayerConfig struct {
|
||||||
TemplateDir InterpolatedString `yaml:"templateDir"`
|
TemplateDir InterpolatedString `yaml:"templateDir"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"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
|
package authn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/go-proxy"
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/Masterminds/sprig/v3"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
@ -14,6 +17,8 @@ import (
|
||||||
type Layer struct {
|
type Layer struct {
|
||||||
layerType store.LayerType
|
layerType store.LayerType
|
||||||
auth Authenticator
|
auth Authenticator
|
||||||
|
|
||||||
|
templateDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||||
|
@ -55,6 +60,11 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||||
return
|
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)))
|
logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err)))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
@ -62,6 +72,11 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := l.injectHeaders(r, options, user); err != nil {
|
if err := l.injectHeaders(r, options, user); err != nil {
|
||||||
|
if errors.Is(err, ErrForbidden) {
|
||||||
|
l.renderForbiddenPage(w, r, layer, options, user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.Error(ctx, "could not inject headers", logger.E(errors.WithStack(err)))
|
logger.Error(ctx, "could not inject headers", logger.E(errors.WithStack(err)))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
@ -74,6 +89,11 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||||
return
|
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)))
|
logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err)))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
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
|
// LayerType implements director.MiddlewareLayer
|
||||||
func (l *Layer) LayerType() store.LayerType {
|
func (l *Layer) LayerType() store.LayerType {
|
||||||
return l.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{
|
return &Layer{
|
||||||
layerType: layerType,
|
layerType: layerType,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
|
templateDir: opts.TemplateDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,21 @@ import (
|
||||||
type LayerOptions struct {
|
type LayerOptions struct {
|
||||||
MatchURLs []string `mapstructure:"matchURLs"`
|
MatchURLs []string `mapstructure:"matchURLs"`
|
||||||
Headers HeadersOptions `mapstructure:"headers"`
|
Headers HeadersOptions `mapstructure:"headers"`
|
||||||
|
Templates TemplatesOptions `mapstructure:"templates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeadersOptions struct {
|
type HeadersOptions struct {
|
||||||
Rules []string `mapstructure:"rules"`
|
Rules []string `mapstructure:"rules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TemplatesOptions struct {
|
||||||
|
Forbidden TemplateOptions `mapstructure:"forbidden"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TemplateOptions struct {
|
||||||
|
Block string `mapstructure:"block"`
|
||||||
|
}
|
||||||
|
|
||||||
func DefaultLayerOptions() LayerOptions {
|
func DefaultLayerOptions() LayerOptions {
|
||||||
return LayerOptions{
|
return LayerOptions{
|
||||||
MatchURLs: []string{"*"},
|
MatchURLs: []string{"*"},
|
||||||
|
@ -36,7 +45,13 @@ func DefaultLayerOptions() LayerOptions {
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Templates: TemplatesOptions{
|
||||||
|
Forbidden: TemplateOptions{
|
||||||
|
Block: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 (
|
import (
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LayerOptions struct {
|
type LayerOptions struct {
|
||||||
MatchURLs []string `mapstructure:"matchURLs"`
|
authn.LayerOptions
|
||||||
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
|
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
|
||||||
TemplateBlock string `mapstructure:"templateBlock"`
|
TemplateBlock string `mapstructure:"templateBlock"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||||
layerOptions := LayerOptions{
|
layerOptions := LayerOptions{
|
||||||
MatchURLs: []string{"*"},
|
LayerOptions: authn.DefaultLayerOptions(),
|
||||||
AuthorizedCIDRs: []string{},
|
AuthorizedCIDRs: []string{},
|
||||||
TemplateBlock: "default",
|
TemplateBlock: "default",
|
||||||
}
|
}
|
|
@ -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},
|
||||||
|
)
|
||||||
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
package circuitbreaker
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
|
@ -1,4 +1,4 @@
|
||||||
package circuitbreaker
|
package authn
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
TemplateDir string
|
TemplateDir string
|
||||||
|
@ -6,10 +6,16 @@ type Options struct {
|
||||||
|
|
||||||
type OptionFunc func(*Options)
|
type OptionFunc func(*Options)
|
||||||
|
|
||||||
func defaultOptions() *Options {
|
func NewOptions(funcs ...OptionFunc) *Options {
|
||||||
return &Options{
|
opts := &Options{
|
||||||
TemplateDir: "./templates",
|
TemplateDir: "./templates",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithTemplateDir(templateDir string) OptionFunc {
|
func WithTemplateDir(templateDir string) OptionFunc {
|
|
@ -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{}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
# Temps de vie par défaut d'une session
|
||||||
defaultKeepAlive: 1m
|
defaultKeepAlive: 1m
|
||||||
|
|
||||||
# Configuration du layer "circuitbreaker"
|
# Configuration du layer "authn"
|
||||||
circuitbreaker:
|
authn:
|
||||||
# Répertoire contenant les templates
|
# 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
|
# Configuration d'une série de proxy/layers
|
||||||
# à créer par défaut par le serveur d'administration
|
# à créer par défaut par le serveur d'administration
|
||||||
|
|
Loading…
Reference in New Issue