feat: add layer definition api
This commit is contained in:
@ -347,3 +347,52 @@ Supprimer le layer
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/layer_route.go#deleteLayer()`](../../../internal/admin/layer_route.go#deleteLayer)
|
||||
|
||||
### `GET /api/v1/definitions/layers?types={type1,type2,...}`
|
||||
|
||||
Lister les définitions des types de layer disponibles.
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{types}` - Optionnel - Liste des types de layer à appliquer en tant que filtre
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"definitions": [
|
||||
{
|
||||
"type": "queue",
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capacity": {
|
||||
"title": "Capacité d'accueil de la file d'attente",
|
||||
"default": 1000,
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"keepAlive": {
|
||||
"title": "Temps de vie d'une session utilisateur sans activité",
|
||||
"description": "Durée sous forme de chaîne de caractères. Voir https://pkg.go.dev/time#ParseDuration pour plus d'informations.",
|
||||
"default": "1m",
|
||||
"type": "string"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -5,7 +5,9 @@ Dans ce tutoriel, nous verrons comment implémenter un layer personnalisé qui p
|
||||
## Prérequis
|
||||
|
||||
Avoir un environnement de développement local fonctionnel. Voir tutoriel ["Démarrer avec les sources"](./getting-started-with-sources.md).
|
||||
|
||||
## Étapes
|
||||
|
||||
### Préparer la structure de base du nouveau layer
|
||||
|
||||
Une implémetation d'un layer se compose majoritairement de 3 éléments:
|
||||
@ -16,120 +18,118 @@ Une implémetation d'un layer se compose majoritairement de 3 éléments:
|
||||
|
||||
1. Créer le répertoire du `package` Go qui contiendra le code de votre layer. Celui ci s'appelera `basicauth`:
|
||||
|
||||
```
|
||||
mkdir -p internal/proxy/director/layer/basicauth
|
||||
```
|
||||
```
|
||||
mkdir -p internal/proxy/director/layer/basicauth
|
||||
```
|
||||
|
||||
2. Créer la structure de base du layer:
|
||||
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
||||
|
||||
package basicauth
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
)
|
||||
import (
|
||||
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
)
|
||||
|
||||
// On définit le "type" (la chaîne de caractères) qui
|
||||
// sera associé à notre layer
|
||||
const LayerType store.LayerType = "basicauth"
|
||||
// On définit le "type" (la chaîne de caractères) qui
|
||||
// sera associé à notre layer
|
||||
const LayerType store.LayerType = "basicauth"
|
||||
|
||||
// On déclare la structure qui
|
||||
// servira de socle pour notre layer
|
||||
type BasicAuth struct{}
|
||||
// On déclare la structure qui
|
||||
// servira de socle pour notre layer
|
||||
type BasicAuth struct{}
|
||||
|
||||
// Les deux méthodes suivantes, attachées à
|
||||
// notre structure BasicAuth, permettent de remplir
|
||||
// le contrat définit par l'interface
|
||||
// director.MiddlewareLayer
|
||||
// Les deux méthodes suivantes, attachées à
|
||||
// notre structure BasicAuth, permettent de remplir
|
||||
// le contrat définit par l'interface
|
||||
// director.MiddlewareLayer
|
||||
|
||||
// LayerType implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
// LayerType implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
|
||||
// C'est dans la méthode "Middleware" qu'on pourra implémenter la
|
||||
// logique appliquée par notre layer.
|
||||
// En l'état actuel l'exécution de la méthode provoquera un panic().
|
||||
// C'est dans la méthode "Middleware" qu'on pourra implémenter la
|
||||
// logique appliquée par notre layer.
|
||||
// En l'état actuel l'exécution de la méthode provoquera un panic().
|
||||
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
panic("unimplemented")
|
||||
}
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func New() *BasicAuth {
|
||||
return &BasicAuth{}
|
||||
}
|
||||
func New() *BasicAuth {
|
||||
return &BasicAuth{}
|
||||
}
|
||||
|
||||
// Cette déclaration permet de profiter
|
||||
// des capacités du compilateur pour s'assurer
|
||||
// que la structure BasicAuth remplie toujours le
|
||||
// contrat imposé par l'interface director.MiddlewareLayer
|
||||
var _ director.MiddlewareLayer = &BasicAuth{}
|
||||
```
|
||||
// Cette déclaration permet de profiter
|
||||
// des capacités du compilateur pour s'assurer
|
||||
// que la structure BasicAuth remplie toujours le
|
||||
// contrat imposé par l'interface director.MiddlewareLayer
|
||||
var _ director.MiddlewareLayer = &BasicAuth{}
|
||||
```
|
||||
|
||||
3. Créer le schéma JSON qui sera associé aux options possibles pour notre layer:
|
||||
|
||||
```json
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
||||
```json
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
||||
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
|
||||
"title": "BasicAuth layer options",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
|
||||
4. Puis créer le fichier Go qui embarquera ces données dans notre binaire via le package [`embed`](https://pkg.go.dev/embed):
|
||||
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer_options.go
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer_options.go
|
||||
|
||||
package basicauth
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
||||
```
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
||||
```
|
||||
|
||||
5. Enfin, créer le fichier d'amorçage pour référencer notre nouveau layer avec Bouncer:
|
||||
|
||||
```go
|
||||
// Fichier internal/setup/basicauth_layer.go
|
||||
```go
|
||||
// Fichier internal/setup/basicauth_layer.go
|
||||
|
||||
package setup
|
||||
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/basicauth"
|
||||
)
|
||||
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/basicauth"
|
||||
)
|
||||
|
||||
// On créait une function d'initialisation qui enregistra les éléments suivants auprès de Bouncer:
|
||||
// - Notre nouveau type de layer
|
||||
// - Une fonction capable de créer une instance de notre layer
|
||||
// - Le schéma associé aux options de notre layer
|
||||
func init() {
|
||||
RegisterLayer(basicauth.LayerType, setupBasicAuthLayer, basicauth.RawLayerOptionsSchema)
|
||||
}
|
||||
// On créait une function d'initialisation qui enregistra les éléments suivants auprès de Bouncer:
|
||||
// - Notre nouveau type de layer
|
||||
// - Une fonction capable de créer une instance de notre layer
|
||||
// - Le schéma associé aux options de notre layer
|
||||
func init() {
|
||||
RegisterLayer(basicauth.LayerType, setupBasicAuthLayer, basicauth.RawLayerOptionsSchema)
|
||||
}
|
||||
|
||||
// La fonction de création de notre layer
|
||||
// reçoit automatiquement la configuration actuelle de Bouncer.
|
||||
|
||||
// Une layer plus avancé pourrait être configurable de cette manière
|
||||
// en créant une nouvelle section de configuration dédiée.
|
||||
func setupBasicAuthLayer(conf *config.Config) (director.Layer, error) {
|
||||
return &basicauth.BasicAuth{}, nil
|
||||
}
|
||||
```
|
||||
// La fonction de création de notre layer
|
||||
// reçoit automatiquement la configuration actuelle de Bouncer.
|
||||
|
||||
// Une layer plus avancé pourrait être configurable de cette manière
|
||||
// en créant une nouvelle section de configuration dédiée.
|
||||
func setupBasicAuthLayer(conf *config.Config) (director.Layer, error) {
|
||||
return &basicauth.BasicAuth{}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Tester l'intégration de notre nouveau layer
|
||||
|
||||
@ -137,41 +137,41 @@ Une implémetation d'un layer se compose majoritairement de 3 éléments:
|
||||
|
||||
1. Vérifier que notre layer est bien référencé en exécutant la commande:
|
||||
|
||||
```
|
||||
./bin/bouncer admin layer create --help
|
||||
```
|
||||
```
|
||||
./bin/bouncer admin layer create --help
|
||||
```
|
||||
|
||||
La sortie devrait ressembler à:
|
||||
La sortie devrait ressembler à:
|
||||
|
||||
```
|
||||
NAME:
|
||||
bouncer admin layer create - Create layer
|
||||
```
|
||||
NAME:
|
||||
bouncer admin layer create - Create layer
|
||||
|
||||
USAGE:
|
||||
bouncer admin layer create [command options] [arguments...]
|
||||
USAGE:
|
||||
bouncer admin layer create [command options] [arguments...]
|
||||
|
||||
OPTIONS:
|
||||
--layer-type LAYER_TYPE Set LAYER_TYPE as layer's type (available: [basicauth queue])
|
||||
[...]
|
||||
```
|
||||
OPTIONS:
|
||||
--layer-type LAYER_TYPE Set LAYER_TYPE as layer's type (available: [basicauth queue])
|
||||
[...]
|
||||
```
|
||||
|
||||
Comme vous devriez le voir nous pouvons désormais créer des layers de type `basicauth`.
|
||||
Comme vous devriez le voir nous pouvons désormais créer des layers de type `basicauth`.
|
||||
|
||||
2. Créer un proxy puis une instance de notre nouveau layer associée à celui ci:
|
||||
|
||||
```bash
|
||||
# Créer un proxy
|
||||
./bin/bouncer admin proxy create --proxy-name cadoles --proxy-to https://www.cadoles.com
|
||||
```bash
|
||||
# Créer un proxy
|
||||
./bin/bouncer admin proxy create --proxy-name cadoles --proxy-to https://www.cadoles.com
|
||||
|
||||
# Activer celui-ci
|
||||
./bin/bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
|
||||
# Activer celui-ci
|
||||
./bin/bouncer admin proxy update --proxy-name cadoles --proxy-enabled=true
|
||||
|
||||
# Ajouter un layer de notre nouveau type à notre proxy
|
||||
./bin/bouncer admin layer create --proxy-name cadoles --layer-name mybasicauth --layer-type basicauth
|
||||
# Ajouter un layer de notre nouveau type à notre proxy
|
||||
./bin/bouncer admin layer create --proxy-name cadoles --layer-name mybasicauth --layer-type basicauth
|
||||
|
||||
# Activer notre nouveau layer
|
||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-enabled=true
|
||||
```
|
||||
# Activer notre nouveau layer
|
||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-enabled=true
|
||||
```
|
||||
|
||||
**Notre layer est actif** ! Cependant il est loin d'être fonctionnel. En effet, si vous faites un:
|
||||
|
||||
@ -188,7 +188,7 @@ Vous n'aurez guère en retour qu'un:
|
||||
> Host: localhost:8080
|
||||
> User-Agent: curl/8.1.2
|
||||
> Accept: */*
|
||||
>
|
||||
>
|
||||
* Empty reply from server
|
||||
* Closing connection 0
|
||||
curl: (52) Empty reply from server
|
||||
@ -210,237 +210,234 @@ Nous allons modifier la méthode `Middleware(layer *store.Layer) proxy.Middlewar
|
||||
|
||||
1. Modifier le fichier contenant la structure de notre layer de la manière suivante:
|
||||
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
||||
|
||||
// [...]
|
||||
// [...]
|
||||
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
// La méthode doit retourner un "Middleware" qui est un alias
|
||||
// pour les fonctions généralement utilisées
|
||||
// dans les librairies http en Go pour créer
|
||||
// une fonction d'interception/transformation de requête.
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// On récupère les identifiants "basic auth" transmis (ou non)
|
||||
// avec la requête
|
||||
username, password, ok := r.BasicAuth()
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
// La méthode doit retourner un "Middleware" qui est un alias
|
||||
// pour les fonctions généralement utilisées
|
||||
// dans les librairies http en Go pour créer
|
||||
// une fonction d'interception/transformation de requête.
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// On récupère les identifiants "basic auth" transmis (ou non)
|
||||
// avec la requête
|
||||
username, password, ok := r.BasicAuth()
|
||||
|
||||
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
|
||||
unauthorized := func() {
|
||||
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
|
||||
// de la popup d'authentification dans le navigateur web de l'utilisateur.
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
|
||||
unauthorized := func() {
|
||||
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
|
||||
// de la popup d'authentification dans le navigateur web de l'utilisateur.
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
|
||||
// On retoure un code d'erreur HTTP 401 (Unauthorized)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
// On retoure un code d'erreur HTTP 401 (Unauthorized)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// L'entête Authorization est absente ou ne correspondant
|
||||
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
|
||||
// on interrompt le traitement de la requête ici
|
||||
unauthorized()
|
||||
if !ok {
|
||||
// L'entête Authorization est absente ou ne correspondant
|
||||
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
|
||||
// on interrompt le traitement de la requête ici
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// On vérifie les identifiants associés à la requête
|
||||
isAuthenticated := authenticate(username, password)
|
||||
// On vérifie les identifiants associés à la requête
|
||||
isAuthenticated := authenticate(username, password)
|
||||
|
||||
// Si les identifiants sont non reconnus alors
|
||||
// on interrompt le traitement de la requête
|
||||
if !isAuthenticated {
|
||||
unauthorized()
|
||||
// Si les identifiants sont non reconnus alors
|
||||
// on interrompt le traitement de la requête
|
||||
if !isAuthenticated {
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// La méthode authenticate() prend un couple d'identifiants
|
||||
// est vérifie en temps constant si ceux ci correspondent à un couple
|
||||
// d'identifiants attendus.
|
||||
func authenticate(username, password string) bool {
|
||||
// On génère une empreinte au format sha256 pour nos identifiants
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
// La méthode authenticate() prend un couple d'identifiants
|
||||
// est vérifie en temps constant si ceux ci correspondent à un couple
|
||||
// d'identifiants attendus.
|
||||
func authenticate(username, password string) bool {
|
||||
// On génère une empreinte au format sha256 pour nos identifiants
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
|
||||
// On effectue de même avec les identifiants attendus.
|
||||
// Pour l'instant, on utilise un couple d'identifiants en "dur".
|
||||
expectedUsernameHash := sha256.Sum256([]byte("foo"))
|
||||
expectedPasswordHash := sha256.Sum256([]byte("baz"))
|
||||
// On effectue de même avec les identifiants attendus.
|
||||
// Pour l'instant, on utilise un couple d'identifiants en "dur".
|
||||
expectedUsernameHash := sha256.Sum256([]byte("foo"))
|
||||
expectedPasswordHash := sha256.Sum256([]byte("baz"))
|
||||
|
||||
// On utilise la méthode subtle.ConstantTimeCompare()
|
||||
// pour faire la comparaison des identifiants en temps constant
|
||||
// et ainsi éviter les attaques par timing.
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
|
||||
// On utilise la méthode subtle.ConstantTimeCompare()
|
||||
// pour faire la comparaison des identifiants en temps constant
|
||||
// et ainsi éviter les attaques par timing.
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
|
||||
|
||||
// L'utilisateur est authentifié si son nom et son mot de passe
|
||||
// correspondent avec ceux attendus.
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
```
|
||||
// L'utilisateur est authentifié si son nom et son mot de passe
|
||||
// correspondent avec ceux attendus.
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
```
|
||||
|
||||
2. Dans votre navigateur, essayez d'ouvrir l'URL http://127.0.0.1:8080. La popup d'authentification devrait s'afficher et vous devriez pouvoir utiliser les identifiants définis dans la fonction `authenticate()` pour vous authentifier et accéder au site de Cadoles !
|
||||
|
||||
> **Note** Essayez de désactiver le layer. L'authentification est automatiquement désactivée également !
|
||||
> **Note** Essayez de désactiver le layer. L'authentification est automatiquement désactivée également !
|
||||
|
||||
## Déclarer des options pour pouvoir utiliser des identifiants dynamiques
|
||||
|
||||
En l'état actuel notre layer est fonctionnel. Cependant il souffre d'un problème notable: les identifiants attendus sont statiques et embarqués en dur dans le code. Nous allons utiliser le schéma associé à nos options, jusqu'alors vide, pour pouvoir créer une paire d'identifiants attendus dynamique.
|
||||
|
||||
|
||||
1. Modifier le schéma JSON des options de notre layer:
|
||||
|
||||
```json
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
||||
```json
|
||||
// Fichier internal/proxy/director/layer/basicauth/layer-options.json
|
||||
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
|
||||
"title": "BasicAuth layer options",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
```
|
||||
|
||||
2. On modifie notre méthode `Middleware()` et la fonction `authenticate()` pour utiliser ces nouvelles options:
|
||||
|
||||
```go
|
||||
package basicauth
|
||||
```go
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "basicauth"
|
||||
const LayerType store.LayerType = "basicauth"
|
||||
|
||||
type BasicAuth struct{}
|
||||
type BasicAuth struct{}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
// LayerType implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) LayerType() store.LayerType {
|
||||
return LayerType
|
||||
}
|
||||
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
// La méthode doit retourner un "Middleware" qui est un alias
|
||||
// pour les fonctions généralement utilisées
|
||||
// dans les librairies http en Go pour créer
|
||||
// une fonction d'interception/transformation de requête.
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// On récupère les identifiants "basic auth" transmis (ou non)
|
||||
// avec la requête
|
||||
username, password, ok := r.BasicAuth()
|
||||
// Middleware implements director.MiddlewareLayer.
|
||||
func (*BasicAuth) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
// La méthode doit retourner un "Middleware" qui est un alias
|
||||
// pour les fonctions généralement utilisées
|
||||
// dans les librairies http en Go pour créer
|
||||
// une fonction d'interception/transformation de requête.
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// On récupère les identifiants "basic auth" transmis (ou non)
|
||||
// avec la requête
|
||||
username, password, ok := r.BasicAuth()
|
||||
|
||||
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
|
||||
unauthorized := func() {
|
||||
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
|
||||
// de la popup d'authentification dans le navigateur web de l'utilisateur.
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
// On créait une méthode locale pour gérer le cas d'une erreur d'authentification.
|
||||
unauthorized := func() {
|
||||
// On ajoute cette entête HTTP à la réponse pour déclencher l'affichage
|
||||
// de la popup d'authentification dans le navigateur web de l'utilisateur.
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
|
||||
// On retoure un code d'erreur HTTP 401 (Unauthorized)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
// On retoure un code d'erreur HTTP 401 (Unauthorized)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// L'entête Authorization est absente ou ne correspondant
|
||||
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
|
||||
// on interrompt le traitement de la requête ici
|
||||
unauthorized()
|
||||
if !ok {
|
||||
// L'entête Authorization est absente ou ne correspondant
|
||||
// pas à du Basic Auth, on retourne une erreur HTTP 401 et
|
||||
// on interrompt le traitement de la requête ici
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// On extrait les identifiants des options associées à notre layer
|
||||
expectedUsername, usernameExists := layer.Options["username"].(string)
|
||||
expectedPassword, passwordExists := layer.Options["password"].(string)
|
||||
// On extrait les identifiants des options associées à notre layer
|
||||
expectedUsername, usernameExists := layer.Options["username"].(string)
|
||||
expectedPassword, passwordExists := layer.Options["password"].(string)
|
||||
|
||||
// Si le nom d'utilisateur ou le mot de passe attendu n'existe pas
|
||||
// alors on retourne une erreur HTTP 500 à l'utilisateur.
|
||||
if !usernameExists || !passwordExists {
|
||||
logger.Error(r.Context(), "basicauth layer missing password or username option")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
// Si le nom d'utilisateur ou le mot de passe attendu n'existe pas
|
||||
// alors on retourne une erreur HTTP 500 à l'utilisateur.
|
||||
if !usernameExists || !passwordExists {
|
||||
logger.Error(r.Context(), "basicauth layer missing password or username option")
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// On vérifie les identifiants associés à la requête
|
||||
isAuthenticated := authenticate(username, password, expectedUsername, expectedPassword)
|
||||
// On vérifie les identifiants associés à la requête
|
||||
isAuthenticated := authenticate(username, password, expectedUsername, expectedPassword)
|
||||
|
||||
// Si les identifiants sont non reconnus alors
|
||||
// on interrompt le traitement de la requête
|
||||
if !isAuthenticated {
|
||||
unauthorized()
|
||||
// Si les identifiants sont non reconnus alors
|
||||
// on interrompt le traitement de la requête
|
||||
if !isAuthenticated {
|
||||
unauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(username, password string, expectedUsername, expectedPassword string) bool {
|
||||
// On génère une empreinte au format sha256 pour nos identifiants
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
func authenticate(username, password string, expectedUsername, expectedPassword string) bool {
|
||||
// On génère une empreinte au format sha256 pour nos identifiants
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
|
||||
// On effectue de même avec les identifiants attendus.
|
||||
expectedUsernameHash := sha256.Sum256([]byte(expectedUsername))
|
||||
expectedPasswordHash := sha256.Sum256([]byte(expectedPassword))
|
||||
// On effectue de même avec les identifiants attendus.
|
||||
expectedUsernameHash := sha256.Sum256([]byte(expectedUsername))
|
||||
expectedPasswordHash := sha256.Sum256([]byte(expectedPassword))
|
||||
|
||||
// On utilise la méthode subtle.ConstantTimeCompare()
|
||||
// pour faire la comparaison des identifiants en temps constant
|
||||
// et ainsi éviter les attaques par timing.
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
|
||||
// On utilise la méthode subtle.ConstantTimeCompare()
|
||||
// pour faire la comparaison des identifiants en temps constant
|
||||
// et ainsi éviter les attaques par timing.
|
||||
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
|
||||
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
|
||||
|
||||
// L'utilisateur est authentifié si son nom et son mot de passe
|
||||
// correspondent avec ceux attendus.
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
// L'utilisateur est authentifié si son nom et son mot de passe
|
||||
// correspondent avec ceux attendus.
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
|
||||
func New() *BasicAuth {
|
||||
return &BasicAuth{}
|
||||
}
|
||||
func New() *BasicAuth {
|
||||
return &BasicAuth{}
|
||||
}
|
||||
|
||||
var _ director.MiddlewareLayer = &BasicAuth{}
|
||||
```
|
||||
var _ director.MiddlewareLayer = &BasicAuth{}
|
||||
```
|
||||
|
||||
3. Modifiez votre layer via la commande d'administration pour déclarer une paire d'identifiants:
|
||||
|
||||
```bash
|
||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-options='{"username":"jdoe","password":"notsosecret"}'
|
||||
```
|
||||
```bash
|
||||
./bin/bouncer admin layer update --proxy-name cadoles --layer-name mybasicauth --layer-options='{"username":"jdoe","password":"notsosecret"}'
|
||||
```
|
||||
|
||||
4. Essayer d'accéder à l'adresse http://127.0.0.1:8080 avec votre navigateur. La popup d'authentification devrait s'afficher et vous devriez pouvoir vous authentifier avec le nouveau couple d'identifiants définis dans les options de votre layer !
|
||||
|
||||
> **Note** Vous pouvez modifier les identifiants plusieurs fois via la commande et vérifier que la fenêtre s'affiche toujours à nouveau, demandant les nouveaux identifiants.
|
||||
> **Note** Vous pouvez modifier les identifiants plusieurs fois via la commande et vérifier que la fenêtre s'affiche toujours à nouveau, demandant les nouveaux identifiants.
|
||||
|
Reference in New Issue
Block a user