Compare commits
75 Commits
basicauth-
...
v2024.5.17
Author | SHA1 | Date | |
---|---|---|---|
af34ee2473 | |||
de70fa89f7 | |||
bb5796ab8c | |||
83fcb9a39d | |||
ad907576dc | |||
3a894972f1 | |||
274bef13d8 | |||
f548c8c8e7 | |||
a82fe46fa3 | |||
cc20bdd289 | |||
7de166765b | |||
35717429a2 | |||
16305469c5 | |||
7515be9583 | |||
e76a82668d | |||
d8b78ad277 | |||
61012b07cd | |||
d12ebfc642 | |||
441d3a623e | |||
e1d9acb980 | |||
f8be2c08d6 | |||
bc7422a50c | |||
9d32551ec5 | |||
ded6d179c1 | |||
6f4ee0ebd1 | |||
1375c9b317 | |||
53a0d26a47 | |||
87354ef0d4 | |||
8560041598 | |||
0611cc9f70 | |||
734ed64e8e | |||
c8fc143efa | |||
f91c14e5d4 | |||
1602626e8c | |||
e2e38841f4 | |||
c23d8e3adb | |||
a3f44cf123 | |||
5453988419 | |||
1e392f94a7 | |||
b44ff2a68e | |||
c719fdca37 | |||
2b91c1e167 | |||
cebf1daf72 | |||
6734cf6526 | |||
368273f1ee | |||
553513d647 | |||
60487c11d6 | |||
e6f18e7cd8 | |||
a207291c04 | |||
64b5182f8b | |||
ce2c19f9b3 | |||
1ffec1f173 | |||
aab5452fa2 | |||
a176b754cd | |||
7b04eb2418 | |||
f8d9ff15b5 | |||
5bd7cbc132 | |||
1b06f07ce8 | |||
82228fd115 | |||
15daddbe13 | |||
5a7062d53e | |||
74409f18e8 | |||
ab7f64a684 | |||
d5cc15de3b | |||
56609ec316 | |||
5bf391b6bf | |||
74928fe413 | |||
ff1d01828d | |||
851f5d64cc | |||
e0d81c061b | |||
440d467938 | |||
f8d33299b9 | |||
6fed6358b2 | |||
ef869a02ea | |||
6559d1f594 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
||||
/admin-key.json
|
||||
/config.yml
|
||||
/tools
|
||||
/out
|
||||
/dist
|
||||
/data
|
||||
/bin
|
||||
/.bouncer-token
|
||||
/.env
|
||||
/misc/k8s
|
||||
/misc/k6s
|
||||
/misc/grafterm
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,4 +7,5 @@
|
||||
/admin-key.json
|
||||
/.bouncer-token
|
||||
/data
|
||||
/out
|
||||
/out
|
||||
.dockerconfigjson
|
||||
|
@ -11,7 +11,7 @@ builds:
|
||||
- -s
|
||||
- -w
|
||||
- -X 'main.GitRef={{ .Commit }}'
|
||||
- -X 'main.ProjectVersion={{ .Version }}'
|
||||
- -X 'main.ProjectVersion={{ .Version }}'
|
||||
- -X 'main.BuildDate={{ .Date }}'
|
||||
- -X 'main.DefaultConfigPath=/etc/bouncer/config.yml'
|
||||
gcflags:
|
||||
@ -33,15 +33,15 @@ archives:
|
||||
- README.md
|
||||
- misc/packaging/common/config.yml
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
name_template: "{{ .Version }}"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
nfpms:
|
||||
- id: bouncer-bin
|
||||
builds:
|
||||
@ -63,6 +63,10 @@ nfpms:
|
||||
- src: layers
|
||||
dst: /etc/bouncer/layers
|
||||
type: config
|
||||
- dst: /etc/bouncer/bootstrap.d
|
||||
type: dir
|
||||
file_info:
|
||||
mode: 0700
|
||||
- id: bouncer-admin
|
||||
meta: true
|
||||
package_name: bouncer-admin
|
||||
|
49
Dockerfile
49
Dockerfile
@ -1,30 +1,55 @@
|
||||
FROM golang:1.19 AS BUILD
|
||||
FROM reg.cadoles.com/proxy_cache/library/golang:1.22.0 AS BUILD
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y make
|
||||
|
||||
COPY . /src
|
||||
ARG YQ_VERSION=4.34.1
|
||||
|
||||
RUN mkdir -p /usr/local/bin \
|
||||
&& wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64 \
|
||||
&& chmod +x /usr/local/bin/yq
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . /src
|
||||
|
||||
RUN make GORELEASER_ARGS='build --rm-dist --single-target --snapshot' goreleaser
|
||||
|
||||
FROM busybox:latest AS RUNTIME
|
||||
# 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 '.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 \
|
||||
&& yq -i '.redis.readTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
|
||||
&& yq -i '.redis.dialTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml
|
||||
|
||||
ARG DUMB_INIT_VERSION=1.2.5
|
||||
FROM reg.cadoles.com/proxy_cache/library/alpine:3.19.1 AS RUNTIME
|
||||
|
||||
RUN mkdir -p /usr/local/bin \
|
||||
&& wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64 \
|
||||
&& chmod +x /usr/local/bin/dumb-init
|
||||
RUN apk add --no-cache ca-certificates dumb-init
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1 /app
|
||||
COPY --from=BUILD /src/config.yml /etc/bouncer/config.yml
|
||||
RUN mkdir -p /usr/local/bin /usr/share/bouncer/bin /etc/bouncer
|
||||
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/bouncer /usr/share/bouncer/bin/bouncer
|
||||
COPY --from=BUILD /src/layers /usr/share/bouncer/layers
|
||||
COPY --from=BUILD /src/dist/bouncer_linux_amd64_v1/config.yml /etc/bouncer/config.yml
|
||||
|
||||
RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
ENTRYPOINT ["/app/bouncer"]
|
||||
RUN adduser -D -H bouncer
|
||||
|
||||
CMD ["bouncer", "run", "-c", "/etc/bouncer/config.yml"]
|
||||
ENV BOUNCER_CONFIG=/etc/bouncer/config.yml
|
||||
|
||||
USER bouncer
|
||||
|
||||
CMD ["bouncer"]
|
27
Jenkinsfile
vendored
27
Jenkinsfile
vendored
@ -29,7 +29,7 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
stage('Release') {
|
||||
stage('Release binaries and packages') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'master'
|
||||
@ -50,6 +50,31 @@ pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build and release Docker image') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'master'
|
||||
branch 'develop'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
withCredentials([
|
||||
usernamePassword([
|
||||
credentialsId: 'kipp-credentials',
|
||||
usernameVariable: 'DOCKER_REGISTRY_USERNAME',
|
||||
passwordVariable: 'DOCKER_REGISTRY_PASSWORD'
|
||||
])
|
||||
]) {
|
||||
sh """
|
||||
echo '${env.DOCKER_REGISTRY_PASSWORD}' | docker login --username '${env.DOCKER_REGISTRY_USERNAME}' --password-stdin reg.cadoles.com
|
||||
make docker-build docker-release
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
|
55
Makefile
55
Makefile
@ -5,18 +5,25 @@ GITCHLOG_ARGS ?=
|
||||
SHELL := /bin/bash
|
||||
|
||||
BOUNCER_VERSION ?=
|
||||
GIT_VERSION := $(shell git describe --always)
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD)
|
||||
DATE_VERSION := $(shell date +%Y.%-m.%-d)
|
||||
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
|
||||
FULL_VERSION := v$(DATE_VERSION)-$(GIT_COMMIT)$(if $(shell git diff --stat),-dirty,)
|
||||
|
||||
DOCKER_IMAGE_NAME ?= cadoles/bouncer
|
||||
DOCKER_IMAGE_NAME ?= reg.cadoles.com/cadoles/bouncer
|
||||
DOCKER_IMAGE_TAG ?= $(FULL_VERSION)
|
||||
|
||||
GOTEST_ARGS ?= -short
|
||||
|
||||
OPENWRT_DEVICE ?= 192.168.1.1
|
||||
|
||||
watch: tools/modd/bin/modd deps ## Watching updated files - live reload
|
||||
SIEGE_URLS_FILE ?= misc/siege/urls.txt
|
||||
SIEGE_CONCURRENCY ?= 100
|
||||
|
||||
data/bootstrap.d/dummy.yml:
|
||||
mkdir -p data/bootstrap.d
|
||||
cp misc/bootstrap.d/dummy.yml data/bootstrap.d/dummy.yml
|
||||
|
||||
watch: tools/modd/bin/modd deps data/bootstrap.d/dummy.yml ## Watching updated files - live reload
|
||||
( set -o allexport && source .env && set +o allexport && tools/modd/bin/modd )
|
||||
|
||||
.PHONY: test
|
||||
@ -25,16 +32,6 @@ test: test-go ## Executing tests
|
||||
test-go: deps
|
||||
( set -o allexport && source .env && set +o allexport && go test -v -count=1 $(GOTEST_ARGS) ./... )
|
||||
|
||||
test-install-script: tools/bin/bash_unit
|
||||
tools/bin/bash_unit ./misc/script/test_install.sh
|
||||
|
||||
tools/bin/bash_unit:
|
||||
mkdir -p tools/bin
|
||||
cd tools/bin && bash <(curl -s https://raw.githubusercontent.com/pgrange/bash_unit/master/install.sh)
|
||||
|
||||
lint: ## Lint sources code
|
||||
golangci-lint run --enable-all $(LINT_ARGS)
|
||||
|
||||
build: build-bouncer ## Build artefacts
|
||||
|
||||
build-bouncer: deps ## Build executable
|
||||
@ -83,14 +80,13 @@ finish-release:
|
||||
git push --all
|
||||
git push --tags
|
||||
|
||||
install-git-hooks:
|
||||
git config core.hooksPath .githooks
|
||||
|
||||
docker-build:
|
||||
docker build -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) .
|
||||
docker tag $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) $(DOCKER_IMAGE_NAME):latest
|
||||
|
||||
docker-release:
|
||||
docker push $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)
|
||||
docker push $(DOCKER_IMAGE_NAME):latest
|
||||
|
||||
gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
||||
mkdir -p .gitea-release
|
||||
@ -106,12 +102,21 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
|
||||
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
|
||||
GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
|
||||
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_COMMIT)" \
|
||||
GITEA_RELEASE_IS_DRAFT="false" \
|
||||
GITEA_RELEASE_BODY="" \
|
||||
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
||||
tools/gitea-release/bin/gitea-release.sh
|
||||
|
||||
grafterm: tools/grafterm/bin/grafterm
|
||||
tools/grafterm/bin/grafterm -c ./misc/grafterm/dashboard.json -v job=bouncer-proxy -r 5s
|
||||
|
||||
siege:
|
||||
$(eval TMP := $(shell mktemp))
|
||||
cat $(SIEGE_URLS_FILE) | envsubst > $(TMP)
|
||||
siege -i -b -c $(SIEGE_CONCURRENCY) -f $(TMP)
|
||||
rm -rf $(TMP)
|
||||
|
||||
tools/gitea-release/bin/gitea-release.sh:
|
||||
mkdir -p tools/gitea-release/bin
|
||||
curl --output tools/gitea-release/bin/gitea-release.sh https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/gitea/gitea-release.sh
|
||||
@ -121,6 +126,10 @@ tools/modd/bin/modd:
|
||||
mkdir -p tools/modd/bin
|
||||
GOBIN=$(PWD)/tools/modd/bin go install github.com/cortesi/modd/cmd/modd@latest
|
||||
|
||||
tools/grafterm/bin/grafterm:
|
||||
mkdir -p tools/grafterm/bin
|
||||
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
|
||||
|
||||
full-version:
|
||||
@echo $(FULL_VERSION)
|
||||
|
||||
@ -139,4 +148,12 @@ run-redis:
|
||||
redis-shell:
|
||||
docker exec -it \
|
||||
bouncer-redis \
|
||||
redis-cli
|
||||
redis-cli
|
||||
|
||||
run-prometheus:
|
||||
docker kill bouncer-prometheus || exit 0
|
||||
docker run --rm -t \
|
||||
--name bouncer-prometheus \
|
||||
--network host \
|
||||
-v $(PWD)/misc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
|
||||
prom/prometheus
|
@ -3,11 +3,27 @@
|
||||
- [(FR) - Premiers pas](./fr/getting-started.md)
|
||||
- [(FR) - Architecture générale](./fr/general-architecture.md)
|
||||
|
||||
## Exemples
|
||||
|
||||
- [(FR) - Exemple de déploiement multi-noeuds](../misc/docker-compose/README.md)
|
||||
|
||||
## Référence
|
||||
|
||||
- [(FR) - Layers](./fr/references/layers/README.md)
|
||||
- [Fichier de configuration](../misc/packaging/common/config.yml)
|
||||
- [(FR) - Métriques](./fr/references/metrics.md)
|
||||
- [(FR) - Fichier de configuration](../misc/packaging/common/config.yml)
|
||||
- [(FR) - API d'administration](./fr/references/admin_api.md)
|
||||
|
||||
## Tutoriels
|
||||
|
||||
- [(FR) - Ajouter un calque de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
||||
### Utilisation
|
||||
|
||||
- [(FR) - Ajouter un layer de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
||||
- [(FR) - Ajouter une authentification OpenID Connect](./fr/tutorials/add-oidc-authn-layer.md)
|
||||
- [(FR) - Amorçage d'un serveur Bouncer via la configuration](./fr/tutorials/bootstrapping.md)
|
||||
- [(FR) - Intégration avec Kubernetes](./fr/tutorials/kubernetes-integration.md)
|
||||
|
||||
### Développement
|
||||
|
||||
- [(FR) - Démarrer avec les sources](./fr/tutorials/getting-started-with-sources.md)
|
||||
- [(FR) - Créer son propre layer](./fr/tutorials/create-custom-layer.md)
|
||||
|
@ -5,14 +5,16 @@
|
||||
### Déploiement mono-noeud
|
||||
|
||||

|
||||
|
||||
## Terminologie
|
||||
|
||||
Voici une liste des termes utilisés dans le lexique Bouncer.
|
||||
|
||||
### Proxy
|
||||
|
||||
Un "proxy" est une entité logique définissant le relation suivante:
|
||||
|
||||
- Un ou plusieurs patrons de filtrage sous la forme `<host>:<port>`. Ceux ci identifient le ou les domaines associés à l'entité;
|
||||
- Un ou plusieurs patrons de filtrage sous la forme d'un patron d'URL avec le caractère `*` comme caractère générique. Ceux ci identifient le ou les domaines/chemins associés à l'entité;
|
||||
- Une URL cible qui servira de base pour la réécriture des requêtes.
|
||||
|
||||
Un "proxy" peut avoir zéro ou plusieurs "layers" associés.
|
||||
@ -27,4 +29,4 @@ Un "layer" (calque) est une entité logique définissant un traitement à appliq
|
||||
|
||||
Un "layer" peut être activé ou désactivé.
|
||||
|
||||
Un "layer" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire).
|
||||
Un "layer" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire).
|
||||
|
@ -41,7 +41,7 @@
|
||||
5. Tester que le CLI est en capacité d'interroger l'API d'administration
|
||||
|
||||
```bash
|
||||
bouncer admin query proxy
|
||||
bouncer admin proxy query
|
||||
```
|
||||
|
||||
Un message équivalent à celui ci devrait s'afficher:
|
||||
@ -92,4 +92,4 @@
|
||||
|
||||
3. Ouvrir la page `https://<ip_serveur>:8080/` dans un navigateur. Le site Cadoles s'affiche !
|
||||
|
||||
**Bravo, vous avez créé votre premier proxy avec Bouncer !**
|
||||
**Bravo, vous avez créé votre premier proxy avec Bouncer !**
|
||||
|
349
doc/fr/references/admin_api.md
Normal file
349
doc/fr/references/admin_api.md
Normal file
@ -0,0 +1,349 @@
|
||||
# API d'administration
|
||||
|
||||
## Authentification
|
||||
|
||||
L'ensemble des appels aux APIs HTTP du service `bouncer-admin` sont authentifiées via l'utilisation d'un jeton [JWT](https://datatracker.ietf.org/doc/html/rfc7519) signé par la clé privée du serveur.
|
||||
|
||||
Le jeton d'accès doit être transmis avec l'ensemble des appels aux points d'entrée via l'entête HTTP `Authorization` en respectant la forme suivante:
|
||||
|
||||
```
|
||||
Authorization: Bearer <jwt>
|
||||
```
|
||||
|
||||
### Génération d'un jeton d'authentification
|
||||
|
||||
La génération d'un jeton d'authentification s'effectue via la commande suivante:
|
||||
|
||||
```shell
|
||||
bouncer auth create-token --subject "<subject>" --role "<role>"
|
||||
```
|
||||
|
||||
Où:
|
||||
|
||||
- `"<subject>"` est une chaîne de caractère arbitraire ayant pour objectif d'identifier de manière unique l'utilisateur associé au jeton;
|
||||
- `"<role>"` peut prendre une des deux valeurs `reader` ou `writer` correspondant aux droits suivants respectifs:
|
||||
- droit en lecture sur l'ensemble des entités (proxy, layer);
|
||||
- droit en lecture ET en écriture sur l'ensemble des entités.
|
||||
|
||||
## Points d'entrée
|
||||
|
||||
### `POST /api/v1/proxies`
|
||||
|
||||
Créer un nouveau proxy
|
||||
|
||||
#### Exemple de corps de requête
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "myproxy", // OBLIGATOIRE - Nom du proxy
|
||||
"to": "https://www.cadoles.com", // OBLIGATOIRE - Site distant ciblé par le proxy
|
||||
"from": ["*"] // OPTIONNEL - Liste de patrons de filtrage associés au proxy
|
||||
}
|
||||
```
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"proxy": {
|
||||
"name": "myproxy",
|
||||
"weight": 0,
|
||||
"enabled": false,
|
||||
"to": "https://www.cadoles.com",
|
||||
"from": ["*"],
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2018-12-10T13:45:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/proxy_route.go#createProxy()`](../../../internal/admin/proxy_route.go#createProxy)
|
||||
|
||||
### `GET /api/v1/proxies/{proxyName}`
|
||||
|
||||
Récupérer les informations complètes sur un proxy
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{proxyName}` - Nom du proxy
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"proxy": {
|
||||
"name": "myproxy",
|
||||
"weight": 0,
|
||||
"enabled": false,
|
||||
"to": "https://www.cadoles.com",
|
||||
"from": ["*"],
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2018-12-10T13:45:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/proxy_route.go#getProxy()`](../../../internal/admin/proxy_route.go#getProxy)
|
||||
|
||||
### `PUT /api/v1/proxies/{proxyName}`
|
||||
|
||||
Modifier un proxy
|
||||
|
||||
#### Exemple de corps de requête
|
||||
|
||||
```json
|
||||
{
|
||||
"to": "https://www.cadoles.com", // OPTIONNEL - Site distant ciblé par le proxy
|
||||
"from": ["mylocalproxydomain:*"], // OPTIONNEL - Liste de patrons de filtrage associés au proxy
|
||||
"weight": 100, // OPTIONNEL - Poids à associer au proxy
|
||||
"enabled": true // OPTIONNEL - Activer/désactiver le proxy
|
||||
}
|
||||
```
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"proxy": {
|
||||
"name": "myproxy",
|
||||
"weight": 100,
|
||||
"enabled": true,
|
||||
"to": "https://www.cadoles.com",
|
||||
"from": ["mylocalproxydomain:*"],
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2020-10-02T15:09:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/proxy_route.go#updateProxy()`](../../../internal/admin/proxy_route.go#updateProxy)
|
||||
|
||||
### `GET /api/v1/proxies?names={name1,name2,...}`
|
||||
|
||||
Lister les proxies existants
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{names}` - Optionnel - Liste des noms de proxy à appliquer en tant que filtre
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"proxies": [
|
||||
{
|
||||
"name": "myproxy",
|
||||
"weight": 0,
|
||||
"enabled": false,
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2018-12-10T13:45:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/proxy_route.go#queryProxy()`](../../../internal/admin/proxy_route.go#queryProxy)
|
||||
|
||||
## `DELETE /api/v1/proxies/{proxyName}`
|
||||
|
||||
Supprimer le proxy
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{proxyName}` - Nom du proxy
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"proxyName": "myproxy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/proxy_route.go#deleteProxy()`](../../../internal/admin/proxy_route.go#deleteProxy)
|
||||
|
||||
### `POST /api/v1/proxies/{proxyName}/layers`
|
||||
|
||||
Créer un nouveau layer pour un proxy donné
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{proxyName}` - Nom du proxy sur lequel créer le layer
|
||||
|
||||
#### Exemple de corps de requête
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mylayer", // OBLIGATOIRE - Nom du layer
|
||||
"type": "<layer_type>", // OBLIGATOIRE - Type du layer, voir doc/fr/references/layers
|
||||
"options": {} // OPTIONNEL - Options associées au layer, voir doc/fr/references/layers
|
||||
}
|
||||
```
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"layer": {
|
||||
"name": "mylayer",
|
||||
"type": "<layer_type>",
|
||||
"enabled": false,
|
||||
"weight": 0,
|
||||
"options": {},
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2018-12-10T13:45:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/layer_route.go#createLayer()`](../../../internal/admin/layer_route.go#createLayer)
|
||||
|
||||
### `GET /api/v1/proxies/{proxyName}/layers/{layerName}`
|
||||
|
||||
Récupérer les informations complètes sur un layer
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{proxyName}` - Nom du proxy parent
|
||||
- `{layerName}` - Nom du layer
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"layer": {
|
||||
"name": "mylayer",
|
||||
"type": "<layer_type>",
|
||||
"enabled": false,
|
||||
"weight": 0,
|
||||
"options": {},
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2018-12-10T13:45:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/layer_route.go#getLayer()`](../../../internal/admin/layer_route.go#getLayer)
|
||||
|
||||
### `PUT /api/v1/proxies/{proxyName}/layers/{layerName}`
|
||||
|
||||
Modifier un layer
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{proxyName}` - Nom du proxy parent
|
||||
- `{layerName}` - Nom du layer
|
||||
|
||||
#### Exemple de corps de requête
|
||||
|
||||
```json
|
||||
{
|
||||
"weight": 100, // OPTIONNEL - Poids à associer au layer
|
||||
"enabled": true, // OPTIONNEL - Activer/désactiver le layer
|
||||
"options": {} // OPTIONNEL - Modifier les options associées au layer, voir doc/fr/references/layers
|
||||
}
|
||||
```
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"layer": {
|
||||
"name": "mylayer",
|
||||
"type": "<layer_type>",
|
||||
"enabled": false,
|
||||
"weight": 0,
|
||||
"options": {},
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2018-12-10T13:45:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/layer_route.go#updateLayer()`](../../../internal/admin/layer_route.go#updateLayer)
|
||||
|
||||
### `GET /api/v1/proxies/{proxyName}/layers?names={name1,name2,...}`
|
||||
|
||||
Lister les layers existants
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{proxyName}` - Nom du proxy parent
|
||||
- `{names}` - Optionnel - Liste des noms de proxy à appliquer en tant que filtre
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"layers": [
|
||||
{
|
||||
"name": "mylayer",
|
||||
"weight": 0,
|
||||
"enabled": false,
|
||||
"createdAt": "2018-12-10T13:45:00.000Z",
|
||||
"updatedAt": "2018-12-10T13:45:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/layer_route.go#queryLayers()`](../../../internal/admin/layer_route.go#queryLayers)
|
||||
|
||||
## `DELETE /api/v1/proxies/{proxyName}/layers/{layerName}`
|
||||
|
||||
Supprimer le layer
|
||||
|
||||
#### Paramètres
|
||||
|
||||
- `{proxyName}` - Nom du proxy parent
|
||||
- `{layerName}` - Nom du layer
|
||||
|
||||
#### Exemple de résultat
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"layerName": "mylayer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Source
|
||||
|
||||
Voir [`internal/admin/layer_route.go#deleteLayer()`](../../../internal/admin/layer_route.go#deleteLayer)
|
@ -2,4 +2,6 @@
|
||||
|
||||
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
|
||||
|
||||
- [Queue](./queue) - File d'attente dynamique
|
||||
- [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
|
||||
|
61
doc/fr/references/layers/authn/README.md
Normal file
61
doc/fr/references/layers/authn/README.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Les layers `authn-*`
|
||||
|
||||
Les layers `authn-*` permettent d'activer différents modes d'authentification au sein d'un proxy Bouncer.
|
||||
|
||||
Les informations liées à l'utilisateur authentifié sont ensuite injectables dans les entêtes HTTP de la requête permettant ainsi une authentification unique("SSO") basée sur les entêtes HTTP ("Trusted headers SSO").
|
||||
|
||||
## Layers
|
||||
|
||||
- [`authn-oidc`](./oidc.md) - Authentification OpenID Connect
|
||||
|
||||
## Schéma des options
|
||||
|
||||
En plus de leurs options spécifiques tous les layers `authn-*` partagent un certain nombre d'options communes.
|
||||
|
||||
Voir le [schéma](../../../../../internal/proxy/director/layer/authn/layer-options.json).
|
||||
|
||||
## Règles d'injection d'entêtes
|
||||
|
||||
L'option `headers.rules` permet de définir une liste de règles utilisant un DSL permettant de définir de manière dynamique quels entêtes seront injectés dans la requête transitant par le layer à destination du service distant.
|
||||
|
||||
La liste des instructions est exécutée séquentiellement.
|
||||
|
||||
Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus des fonctionnalités natives du langage, Bouncer ajoute un certain nombre de fonction spécifique à son contexte.
|
||||
|
||||
Le comportement des règles par défaut est le suivant:
|
||||
|
||||
1. L'ensemble des entêtes HTTP correspondant au patron `Remote-*` sont supprimés ;
|
||||
2. L'identifiant de l'utilisateur identifié (`user.subject`) est exporté sous la forme de l'entête HTTP `Remote-User` ;
|
||||
3. L'ensemble des attributs de l'utilisateur identifié (`user.attrs`) sont exportés sous la forme `Remote-User-Attr-<name>` où `<name>` est le nom de l'attribut en minuscule, avec les `_` transformés en `-`.
|
||||
|
||||
### Fonctions
|
||||
|
||||
#### `set_header(name string, value string)`
|
||||
|
||||
Définir la valeur d'une entête HTTP via son nom `name` et sa valeur `value`.
|
||||
|
||||
#### `del_headers(pattern string)`
|
||||
|
||||
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
|
||||
|
||||
Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires.
|
||||
|
||||
### Environnement
|
||||
|
||||
Les règles ont accès aux variables suivantes pendant leur exécution.
|
||||
|
||||
#### `user`
|
||||
|
||||
L'utilisateur identifié par le layer.
|
||||
|
||||
```json
|
||||
{
|
||||
// Identifiant de l'utilisateur, tel que récupéré par le layer
|
||||
"subject": "<string>",
|
||||
// Table associative des attributs associés à l'utilisateur
|
||||
// La liste de ces attributs dépend du layer d'authentification
|
||||
"attrs": {
|
||||
"key": "<value>"
|
||||
}
|
||||
}
|
||||
```
|
72
doc/fr/references/layers/authn/oidc.md
Normal file
72
doc/fr/references/layers/authn/oidc.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Layer `authn-oidc`
|
||||
|
||||
## Description
|
||||
|
||||
Ce layer permet d'ajouter une authentification OpenID Connect au service distant.
|
||||
|
||||
Voir le tutoriel ["Ajouter une authentification OpenID Connect"](../../../tutorials/add-oidc-authn-layer.md) pour plus d'informations quant à son utilisation.
|
||||
|
||||
## Type
|
||||
|
||||
`authn-oidc`
|
||||
|
||||
## 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/oidc/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 valué avec la valeur [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) `sub` extrait de l'`idToken` récupéré lors de l'authentification ;
|
||||
- `user.attrs` comportera les propriétés suivantes:
|
||||
|
||||
- L'ensemble des `claims` provenant de l'`idToken` seront transposés en `claim_<name>` (ex: `idToken.iss` sera transposé en `user.attrs.claim_iss`) ;
|
||||
- `user.attrs.access_token`: le jeton d'accès associé à l'authentification ;
|
||||
- `user.attrs.refresh_token`: le jeton de rafraîchissement associé à l'authentification (si disponible, en fonction des `scopes` demandés par le client) ;
|
||||
- `user.attrs.token_expiry`: Horodatage Unix (en secondes) associé à la date d'expiration du jeton d'accès ;
|
||||
- `user.attrs.logout_url`: URL de déconnexion pour la suppression de la session Bouncer.
|
||||
|
||||
**Attention** Cette URL ne permet dans la plupart des cas que de supprimer la session côté Bouncer. La suppression de la session côté fournisseur d'identité est conditionné à la présence ou non de l'attribut [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0-17.html#OPMetadata) dans les données du endpoint `.wellknown/openid-configuration`.
|
||||
|
||||
## Métriques
|
||||
|
||||
Les [métriques Prometheus](../../metrics.md) suivantes sont exposées par ce layer.
|
||||
|
||||
### `bouncer_layer_authn_oidc_login_requests_total{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total de demandes d'authentification
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_authn_oidc_login_requests_total Bouncer's authn-oidc layer total login requests
|
||||
# TYPE bouncer_layer_authn_oidc_login_requests_total counter
|
||||
bouncer_layer_authn_oidc_login_requests_total{layer="my-layer",proxy="my-proxy"} 1
|
||||
```
|
||||
|
||||
### `bouncer_layer_authn_oidc_login_successes_total{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total d'authentifications réussies
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_authn_oidc_login_successes_total Bouncer's authn-oidc layer total login successes
|
||||
# TYPE bouncer_layer_authn_oidc_login_successes_total counter
|
||||
bouncer_layer_authn_oidc_login_successes_total{layer="my-layer",proxy="my-proxy"} 1
|
||||
```
|
||||
|
||||
### `bouncer_layer_authn_oidc_logout_total{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total de déconnexions
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_authn_oidc_logout_total Bouncer's authn-oidc layer total logouts
|
||||
# TYPE bouncer_layer_authn_oidc_logout_total counter
|
||||
bouncer_layer_authn_oidc_logout_total{layer="my-layer",proxy="my-proxy"} 1
|
||||
```
|
41
doc/fr/references/layers/circuitbreaker.md
Normal file
41
doc/fr/references/layers/circuitbreaker.md
Normal file
@ -0,0 +1,41 @@
|
||||
# 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._
|
@ -22,6 +22,42 @@ Ce layer permet d'ajouter un mécanisme de file d'attente dynamique au proxy ass
|
||||
- **Valeur par défaut:** `1m`
|
||||
- **Description:** Durée de vie d'une session dans la file d'attente sans activité avant expiration.
|
||||
|
||||
### Schéma
|
||||
### `matchURLs`
|
||||
|
||||
Voir le [schéma JSON](../../../../internal/queue/schema/layer-options.json).
|
||||
- **Type:** `[]string`
|
||||
- **Valeur par défaut:** `["*"]`
|
||||
- **Description:** Limiter l'action de la file d'attente à cette liste de patrons d'URLs.
|
||||
|
||||
Par exemple, si vous souhaitez limiter votre file à 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 cette file d'attente.
|
||||
|
||||
## Schéma
|
||||
|
||||
Voir le [schéma JSON](../../../../internal/proxy/director/layer/queue/schema/layer-options.json).
|
||||
|
||||
## Métriques
|
||||
|
||||
Les [métriques Prometheus](../metrics.md) suivantes sont exposées par ce layer.
|
||||
|
||||
### `bouncer_layer_queue_capacity{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `gauge`
|
||||
- **Description**: Capacité maximale de la queue
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_queue_capacity Bouncer's queue layer capacity
|
||||
# TYPE bouncer_layer_queue_capacity gauge
|
||||
bouncer_layer_queue_capacity{layer="queue",proxy="cadoles"} 2
|
||||
```
|
||||
|
||||
### `bouncer_layer_queue_sessions{layer=<layerName>,proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `gauge`
|
||||
- **Description**: Nombre courant de sessions ouvertes
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_layer_queue_sessions Bouncer's queue layer current sessions
|
||||
# TYPE bouncer_layer_queue_sessions gauge
|
||||
bouncer_layer_queue_sessions{layer="queue",proxy="cadoles"} 3
|
||||
```
|
29
doc/fr/references/metrics.md
Normal file
29
doc/fr/references/metrics.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Métriques
|
||||
|
||||
Bouncer expose un certain nombre de métriques Prometheus sur le serveur proxy ainsi que sur le serveur d'administration. Ces métriques sont par défaut accessibles sur `/.bouncer/metrics`.
|
||||
|
||||
Il est possible de configurer le point d'entrée de ces métriques ainsi que d'ajouter une authentification de type `Basic Auth` [via la configuration](../../../misc/packaging/common/config.yml) (voir les clés `admin.metrics` et `proxy.metrics`).
|
||||
|
||||
Outre les métriques par défaut fournies par la librairie [Prometheus](https://prometheus.io/docs/guides/go-application/#instrumenting-a-go-application-for-prometheus), les serveurs Bouncer exposent également des métriques propres.
|
||||
|
||||
Chaque layer associé à un proxy peut également ses propres métriques spécifiques. [Voir la page de documentation](./layers/README.md) de chaque layer pour plus d'informations.
|
||||
|
||||
## Métriques spécifiques
|
||||
|
||||
### Serveur proxy
|
||||
|
||||
#### `bouncer_proxy_director_proxy_requests_total{proxy=<proxyName>}`
|
||||
|
||||
- **Type:** `counter`
|
||||
- **Description**: Nombre total de requêtes ayant transité par le proxy
|
||||
- **Exemple**
|
||||
|
||||
```
|
||||
# HELP bouncer_proxy_director_proxy_requests_total Bouncer proxy total requests
|
||||
# TYPE bouncer_proxy_director_proxy_requests_total counter
|
||||
bouncer_proxy_director_proxy_requests_total{proxy="cadoles"} 64
|
||||
```
|
||||
|
||||
### Serveur d'administration
|
||||
|
||||
_Pas de métrique supplémentaire._
|
81
doc/fr/tutorials/add-oidc-authn-layer.md
Normal file
81
doc/fr/tutorials/add-oidc-authn-layer.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Ajouter une authentification OpenID Connect
|
||||
|
||||
Dans ce tutoriel nous verrons comment ajouter un layer de type `oidc-authn` à un proxy pour ajouter une authentification OpenID Connect à notre service distant.
|
||||
|
||||
## Prérequis
|
||||
|
||||
### Création d'une application OAuth2
|
||||
|
||||
Pour réaliser ce tutoriel nous allons utiliser la forge Cadoles comme fournisseur d'identité. Vous devrez donc créer une application OAuth2 avec votre compte Cadoles sur https://forge.cadoles.com/user/settings/applications et collecter les informations suivantes:
|
||||
|
||||
- Identifiant du client ;
|
||||
- Secret du client.
|
||||
|
||||
Concernant l'URL de redirection, si vous ne modifiez pas l'option `oidc.loginCallbackPath` vous devrez renseigner une URL répondant au modèle suivant:
|
||||
|
||||
```
|
||||
<base_url>/.bouncer/authn/oidc/<proxy_name>/<layer_name>/callback
|
||||
```
|
||||
|
||||
Où
|
||||
|
||||
- `<base_url>` est l'URL de base d'accès à votre instance Bouncer, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut ;
|
||||
- `<proxy_name>` est le nom du proxy créé dans Bouncer, dans ce tutoriel `my-proxy` ;
|
||||
- `<layer_name>` est le nom du layer créé dans Bouncer, dans ce tutoriel `my-layer`.
|
||||
|
||||
### Démarrer le serveur `dummy` pour l'introspection des entêtes reçus
|
||||
|
||||
Bouncer intègre un serveur de test qui permet l'introspection des entêtes HTTP reçus dans la requête. Nous allons utiliser celui ci comme service distant afin de visualiser les entêtes générés par notre layer d'authentification.
|
||||
|
||||
Pour le lancer:
|
||||
|
||||
```shell
|
||||
# Avec le binaire
|
||||
bouncer server dummy run
|
||||
|
||||
# Avec Docker
|
||||
docker run -it --rm -p 8082:8082 --read-only reg.cadoles.com/cadoles/bouncer:latest bouncer server dummy run
|
||||
```
|
||||
|
||||
Par défaut ce serveur écoute sur le port 8082. Il est possible de modifier l'adresse d'écoute via le drapeau `--address`.
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Avec le client d'administration de Bouncer en ligne de commande, créer un nouveau proxy
|
||||
|
||||
```shell
|
||||
bouncer admin proxy create --proxy-name my-proxy --proxy-to http://localhost:8082
|
||||
```
|
||||
|
||||
Où http://localhost:8082 est l'adresse de notre serveur `dummy` de test, lancé dans les prérequis.
|
||||
|
||||
2. Activer le proxy `my-proxy`
|
||||
|
||||
```shell
|
||||
bouncer admin proxy update --proxy-name my-proxy --proxy-enabled
|
||||
```
|
||||
|
||||
3. À ce stade, vous devriez pouvoir afficher la page du serveur `dummy` en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut
|
||||
|
||||
4. Créer un layer de type `authn-oidc` pour notre nouveau proxy
|
||||
|
||||
```shell
|
||||
bouncer admin layer create --proxy-name my-proxy --layer-name my-layer --layer-type authn-oidc
|
||||
```
|
||||
|
||||
5. Configurer le nouveau layer `my-layer` avec les options collectée dans les prérequis et l'activer
|
||||
|
||||
```shell
|
||||
bouncer admin layer update --proxy-name my-proxy --layer-name my-layer --layer-options '{ "oidc":{"clientId": "<clientId>", "clientSecret":"<clientSecret>", "issuerURL": "https://forge.cadoles.com/" }}' --layer-enabled
|
||||
```
|
||||
|
||||
Où:
|
||||
|
||||
- `<clientId>` est l'identifiant du client OIDC récupéré dans les prérequis ;
|
||||
- `<clientSecret>` est le secret du client OIDC récupéré dans les prérequis.
|
||||
|
||||
6. À ce stade en ouvrant l'URL de votre instance Bouncer vous devriez être redirigé vers la forge Cadoles vous demandant de vous authentifier. Une fois authentifié vous devriez arriver sur la page du serveur `dummy` avec les nouveaux entêtes liés à votre authentification (entêtes `Remote-User-*`).
|
||||
|
||||
## Ressources
|
||||
|
||||
- [Référence du layer `authn-oidc`](../../fr/references/layers/authn/oidc.md)
|
@ -1,8 +1,8 @@
|
||||
# Ajouter un calque de type "file d'attente"
|
||||
# Ajouter un layer de type "file d'attente"
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau calque ("layer") pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
|
||||
1. Sur le serveur hébergeant les services Bouncer, utiliser le CLI pour créer un nouveau layer pour votre proxy. Dans l'exemple, nous utiliserons le proxy `cadoles` créé dans le cadre du tutoriels ["Premiers pas"](../getting-started.md).
|
||||
|
||||
```bash
|
||||
# Création d'un calque nommé 'my-queue' pour le proxy 'cadoles' de type 'queue'
|
||||
@ -19,7 +19,7 @@
|
||||
+----------+-------+---------+--------+---------+-------------------------+-------------------------+
|
||||
```
|
||||
|
||||
2. À ce stade, le calque est encore inactif. Définir la capacité de la file d'attente à 1 et activer le calque en utilisant le CLI
|
||||
2. À ce stade, le layer est encore inactif. Définir la capacité de la file d'attente à 1 et activer le layer en utilisant le CLI:
|
||||
|
||||
```bash
|
||||
bouncer admin layer update --proxy-name cadoles --layer-name my-queue --layer-enabled=true --layer-options '{"capacity": 1}'
|
||||
@ -43,6 +43,6 @@
|
||||
|
||||
3. Le proxy `cadoles` a désormais une file d'attente avec une capacité d'un seul utilisateur. Vous pouvez effectuer le test en ouvrant votre navigateur sur l'adresse `http://<ip_serveur>:8080/` puis en ouvrant une fenêtre de navigation privée sur la même adresse:
|
||||
- La première fenêtre devrait afficher le site Cadoles;
|
||||
- La seconde fenêtre devrait afficher le message suivant: `queued (rank: 2, status: 2/1)`.
|
||||
- La seconde fenêtre devrait afficher une page indiquant qu'on est en file d'attente.
|
||||
|
||||
Si vous laissez expirer la "session" de la première fenêtre (environ 1 minute par défaut) et que vous rafraîchissez la seconde, vous devriez avoir une inversion des états.
|
47
doc/fr/tutorials/bootstrapping.md
Normal file
47
doc/fr/tutorials/bootstrapping.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Amorçage d'un serveur Bouncer via la configuration
|
||||
|
||||
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é.
|
||||
|
||||
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:
|
||||
|
||||
- Utiliser un répertoire contenant des fichiers YAML (un par proxy) en définissant le chemin du répertoire via l'attribut `bootstrap.dir`;
|
||||
- Définir directement la liste des proxies via l'attribut `bootstrap.proxies`.
|
||||
|
||||
```yaml
|
||||
# Configuration d'une série de proxy/layers
|
||||
# à créer par défaut par le serveur d'administration
|
||||
bootstrap:
|
||||
# Répertoire contenant les définitions de proxy à créer
|
||||
# par défaut. Les fichiers seront récupérés si ils
|
||||
# correspondent au patron de nommage suivant:
|
||||
#
|
||||
# <bootstrap_dir>/<proxy_name>.yml
|
||||
#
|
||||
# Voir ci-dessous pour les attributs possibles dans les fichiers.
|
||||
#
|
||||
# Si l'attribut est vide ou absent le chargement des fichiers
|
||||
# est désactivé.
|
||||
dir: /etc/bouncer/bootstrap.d
|
||||
|
||||
# Tableau associatif de définition de proxies à créer par
|
||||
# défaut par le serveur d'administration.
|
||||
# Si `proxies` et `dir` sont tous les deux définis, les fichiers
|
||||
# présents dans le répertoire `dir` surchargeront les valeurs définies
|
||||
# dans `proxies`.
|
||||
#
|
||||
# Par défaut vide.
|
||||
proxies:
|
||||
# my-proxy:
|
||||
# enabled: true # Activer/désactiver le proxy
|
||||
# from: ["*"] # Filtre d'origine d'activation du proxy
|
||||
# to: "https://example.net" # Destination du proxy
|
||||
# weight: 0 # Priorité du proxy
|
||||
# layers: # Layers associés au proxy
|
||||
# my-layer:
|
||||
# type: queue # Type du proxy
|
||||
# enabled: false # Activer/désactiver le layer
|
||||
# weight: 0 # Priorité du layer
|
||||
# options: {"capacity": 100} # Options associées au layer
|
||||
```
|
446
doc/fr/tutorials/create-custom-layer.md
Normal file
446
doc/fr/tutorials/create-custom-layer.md
Normal file
@ -0,0 +1,446 @@
|
||||
# Créer son propre layer
|
||||
|
||||
Dans ce tutoriel, nous verrons comment implémenter un layer personnalisé qui permettra d'ajouter une authentification de type [`Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) à un proxy.
|
||||
|
||||
## 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:
|
||||
|
||||
- Une structure qui implémente une ou plusieurs interfaces (`director.MiddlewareLayer`, `director.RequestTransformerLayer` et/ou `director.ResponseTransformerLayer`);
|
||||
- Un schéma au format [JSON Schema](http://json-schema.org/) qui permettra de valider les "options" de notre layer;
|
||||
- Un fichier d'amorçage qui permettra à Bouncer de référencer notre nouveau layer.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
2. Créer la structure de base du layer:
|
||||
|
||||
```go
|
||||
// Fichier internal/proxy/director/layer/basicauth/basicauth.go
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// 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{}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
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{}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/basicauth-layer-options",
|
||||
"title": "BasicAuth layer options",
|
||||
"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
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//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
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
## Tester l'intégration de notre nouveau layer
|
||||
|
||||
À ce stade, notre nouveau layer est normalement référencé et donc "utilisable" dans Bouncer (si on omet le fait qu'il déclenchera une `panic()`).
|
||||
|
||||
1. Vérifier que notre layer est bien référencé en exécutant la commande:
|
||||
|
||||
```
|
||||
./bin/bouncer admin layer create --help
|
||||
```
|
||||
|
||||
La sortie devrait ressembler à:
|
||||
|
||||
```
|
||||
NAME:
|
||||
bouncer admin layer create - Create layer
|
||||
|
||||
USAGE:
|
||||
bouncer admin layer create [command options] [arguments...]
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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:
|
||||
|
||||
```
|
||||
curl -v http://localhost:8080
|
||||
```
|
||||
|
||||
Vous n'aurez guère en retour qu'un:
|
||||
|
||||
```
|
||||
* Trying 127.0.0.1:8080...
|
||||
* Connected to localhost (127.0.0.1) port 8080 (#0)
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> User-Agent: curl/8.1.2
|
||||
> Accept: */*
|
||||
>
|
||||
* Empty reply from server
|
||||
* Closing connection 0
|
||||
curl: (52) Empty reply from server
|
||||
```
|
||||
|
||||
Et également dans la console où s'exécute le service `bouncer-proxy`, vous aurez le message:
|
||||
|
||||
```
|
||||
2023/06/23 18:39:59 http: panic serving 127.0.0.1:59868: unimplemented
|
||||
```
|
||||
|
||||
**Il est temps d'implémenter réellement la logique associée à notre layer !**
|
||||
|
||||
> **Note** Vous pouvez désactiver votre layer via le drapeau `--layer-enabled=false` et voir le site Cadoles s'afficher à nouveau !
|
||||
|
||||
## Implémenter l'authentification sur notre nouveau layer
|
||||
|
||||
Nous allons modifier la méthode `Middleware(layer *store.Layer) proxy.Middleware` attachée à notre structure `BasicAuth`.
|
||||
|
||||
1. Modifier le fichier contenant la structure de notre layer de la manière suivante:
|
||||
|
||||
```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()
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
// 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)
|
||||
|
||||
// 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 !
|
||||
|
||||
## 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
|
||||
|
||||
{
|
||||
"$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
|
||||
}
|
||||
```
|
||||
|
||||
2. On modifie notre méthode `Middleware()` et la fonction `authenticate()` pour utiliser ces nouvelles options:
|
||||
|
||||
```go
|
||||
package basicauth
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "basicauth"
|
||||
|
||||
type BasicAuth struct{}
|
||||
|
||||
// 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()
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// L'authentification a réussie ! On passe la main au handler HTTP suivant
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
// 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)
|
||||
|
||||
// L'utilisateur est authentifié si son nom et son mot de passe
|
||||
// correspondent avec ceux attendus.
|
||||
return usernameMatch && passwordMatch
|
||||
}
|
||||
|
||||
func New() *BasicAuth {
|
||||
return &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"}'
|
||||
```
|
||||
|
||||
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.
|
132
doc/fr/tutorials/getting-started-with-sources.md
Normal file
132
doc/fr/tutorials/getting-started-with-sources.md
Normal file
@ -0,0 +1,132 @@
|
||||
# Démarrer avec les sources
|
||||
|
||||
Dans ce tutoriel, nous verrons comment lancer un environnement de développement en local sur notre machine afin de travailler sur les sources de Bouncer.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Les éléments suivants doivent être installés sur votre machine:
|
||||
|
||||
- [Golang > 1.20](https://go.dev/)
|
||||
- [Docker](https://www.docker.com/)
|
||||
- [Git](https://git-scm.com/)
|
||||
- [GNU Make](https://www.gnu.org/software/make/)
|
||||
|
||||
Les ports suivants doivent être disponibles sur votre machine:
|
||||
|
||||
- `8080`
|
||||
- `8081`
|
||||
|
||||
## Étapes
|
||||
|
||||
1. Cloner le dépôt des sources du projet Bouncer
|
||||
|
||||
```
|
||||
git clone https://forge.cadoles.com/Cadoles/bouncer
|
||||
```
|
||||
|
||||
2. Se positionner dans le répertoire du projet
|
||||
|
||||
```
|
||||
cd bouncer
|
||||
```
|
||||
|
||||
3. Lancer le projet en mode "développement"
|
||||
|
||||
```
|
||||
make watch
|
||||
```
|
||||
|
||||
Si toutes les dépendances sont correctement installées et configurées sur votre machine, la console devrait afficher une série de messages pour ensuite s'arrêter sur quelque chose ressemblant à:
|
||||
|
||||
```
|
||||
14:47:06: daemon: make run BOUNCER_CMD="--config config.yml server admin run"
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/command/server/admin/run.go:42> RunCommand.func1 listening {"url": "http://127.0.0.1:8081"}
|
||||
2023-06-23 20:47:06.095 [INFO] <./internal/admin/server.go:126> (*Server).run http server listening
|
||||
14:47:06: daemon: make run-redis
|
||||
bouncer-redis
|
||||
docker run --rm -t \
|
||||
--name bouncer-redis \
|
||||
-v /home/wpetit/workspace/bouncer/data/redis:/data \
|
||||
-p 6379:6379 \
|
||||
redis:alpine3.17 \
|
||||
redis-server --save 60 1 --loglevel warning
|
||||
1:C 23 Jun 2023 20:47:06.754 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
|
||||
1:C 23 Jun 2023 20:47:06.754 # Redis version=7.0.11, bits=64, commit=00000000, modified=0, pid=1, just started
|
||||
1:C 23 Jun 2023 20:47:06.754 # Configuration loaded
|
||||
1:M 23 Jun 2023 20:47:06.759 # Warning: Could not create server TCP listening socket ::*:6379: unable to bind socket, errno: 97
|
||||
1:M 23 Jun 2023 20:47:06.760 # Server initialized
|
||||
1:M 23 Jun 2023 20:47:06.760 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect
|
||||
```
|
||||
|
||||
À ce stade, le serveur `bouncer-admin` écoute sur http://127.0.0.1:8081 et le serveur `bouncer-proxy` sur http://127.0.0.1:8080.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> L'outil [`modd`](https://github.com/cortesi/modd) est utilisé pour surveiller les modifications sur les sources et relancer automatiquement la compilation et les services en cas de changement.
|
||||
|
||||
## Commandes `make` utiles
|
||||
|
||||
### `make watch`
|
||||
|
||||
Surveiller les sources, compiler celles ci en cas de modifications et lancer les services `bouncer-proxy` et `bouncer-admin`.
|
||||
|
||||
### `make test`
|
||||
|
||||
Exécuter les tests unitaires/d'intégration du projet.
|
||||
|
||||
### `make build`
|
||||
|
||||
Compiler une version de développement du binaire `bouncer`.
|
||||
|
||||
### `make docker-build`
|
||||
|
||||
Construire une image Docker pour Bouncer.
|
||||
|
||||
Vous pouvez ensuite lancer l'image localement avec la commande:
|
||||
|
||||
```
|
||||
docker run \
|
||||
-it --rm \
|
||||
reg.cadoles.com/cadoles/bouncer:<tag> \
|
||||
-p 8080:8080 \
|
||||
bouncer server proxy run
|
||||
```
|
||||
|
||||
### `make grafterm`
|
||||
|
||||
Afficher un tableau de bord [`grafterm`](https://github.com/slok/grafterm) branché sur l'instance Prometheus locale.
|
||||
|
||||
### `make siege`
|
||||
|
||||
Lancer une session de test [`siege`](https://github.com/JoeDog/siege) sur l'instance `bouncer-proxy` locale.
|
||||
## Arborescence du projet
|
||||
|
||||
```bash
|
||||
.
|
||||
├── bin # Répertoire de destination des binaires Go de développement
|
||||
├── cmd # Package principal (main) du binaire Bouncer
|
||||
├── data # Répertoire des données de développement (Redis)
|
||||
├── dist # Répertoire de destination des archives/paquets pour la publication
|
||||
├── doc # Répertoire de documentation du projet
|
||||
├── internal # Source Go du projet
|
||||
│ ├── admin
|
||||
│ ├── auth
|
||||
│ ├── chi
|
||||
│ ├── client
|
||||
│ ├── command
|
||||
│ ├── config
|
||||
│ ├── format
|
||||
│ ├── imports
|
||||
│ ├── jwk
|
||||
│ ├── proxy
|
||||
│ ├── schema
|
||||
│ ├── setup
|
||||
│ └── store
|
||||
├── layers # Fichiers annexes liés aux layers (templates HTML)
|
||||
│ └── queue
|
||||
├── misc # Fichiers annexes
|
||||
│ ├── jenkins # Fichiers liés au pipeline d'intégration continue Jenkins
|
||||
│ ├── logo # Logo du projet
|
||||
│ └── packaging # Fichiers liés à l'empaquetage des binaires
|
||||
└── tools # Outils utilisés en développement
|
||||
```
|
61
doc/fr/tutorials/kubernetes-integration.md
Normal file
61
doc/fr/tutorials/kubernetes-integration.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Intégration avec Kubernetes
|
||||
|
||||
Dans le cadre du déploiement de Bouncer dans un environnement Kubernetes, il est possible d'activer un mode d'intégration permettant à Bouncer d'exposer des jetons d'authentification directement sous forme de [`Secret`](https://kubernetes.io/fr/docs/concepts/configuration/secret/).
|
||||
|
||||
L'activation et configuration de l'intégration Kubernetes s'effectue dans le fichier de configuration du serveur d'administration via la section `integrations.kubernetes`:
|
||||
|
||||
```yaml
|
||||
# Section de configuration des intégrations
|
||||
# avec des produits externes
|
||||
integrations:
|
||||
# Intégration avec Kubernetes
|
||||
kubernetes:
|
||||
# Activer/désactiver l'intégration Kubernetes
|
||||
enabled: true
|
||||
# Créer/mettre à jour un Secret automatiquement avec un jeton d'authentification
|
||||
# avec le rôle "writer".
|
||||
# Désactivé si l'attribut est vide ou absent.
|
||||
writerTokenSecret: my-bouncer-admin-writer-token
|
||||
# Namespace de destination du Secret pour le jeton d'authentification
|
||||
# avec le rôle "reader".
|
||||
# Utilise par défaut le namespace courant si absent ou vide.
|
||||
writerTokenSecretNamespace: "my-namespace"
|
||||
# Créer/mettre à jour un Secret automatiquement avec un jeton d'authentification
|
||||
# avec le rôle "reader".
|
||||
# Désactivé si l'attribut est vide ou absent.
|
||||
readerTokenSecret: my-bouncer-admin-reader-token
|
||||
# Namespace de destination du Secret pour le jeton d'authentification
|
||||
# avec le rôle "reader".
|
||||
# Utilise par défaut le namespace courant si absent ou vide.
|
||||
readerTokenSecretNamespace: "my-namespace"
|
||||
# Délai maximum alloué au verrou distribué pour la mise à jour
|
||||
# des secrets.
|
||||
lockTimeout: 30s
|
||||
```
|
||||
|
||||
Vous devrez également définir un `ServiceAccount` pour votre `Pod` avec un `Role` équivalent au suivant (dans le cas nominal où le `Pod` créait les `Secrets` dans son même namespace):
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: bouncer-admin
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
- v1
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- update
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> La génération des jetons d'authentification s'effectue à chaque démarrage du serveur d'administration. Un verrou partagé permet d'éviter que plusieurs instances fonctionnant en parallèle essayent de mettre à jour les ressources Kubernetes au même moment.
|
||||
>
|
||||
> De plus, les jetons seront laissés en l'état si la clé de génération n'a pas été modifiée pour éviter de changer les jetons à chaque redémarrage d'un `Pod` (voir l'annotation `bouncer.cadoles.com/public-key` sur les `Secrets` créés.).
|
||||
|
||||
Un exemple fonctionnel de déploiement Kubernetes est disponible dans le répertoire `misc/k8s` du projet.
|
82
go.mod
82
go.mod
@ -1,17 +1,32 @@
|
||||
module forge.cadoles.com/cadoles/bouncer
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/bsm/redislock v0.9.4
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/dchest/uniuri v1.2.0
|
||||
github.com/drone/envsubst v1.0.3
|
||||
github.com/expr-lang/expr v1.16.7
|
||||
github.com/getsentry/sentry-go v0.22.0
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6
|
||||
github.com/mitchellh/mapstructure v1.4.1
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/ory/dockertest/v3 v3.10.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/qri-io/jsonschema v0.2.1
|
||||
github.com/redis/go-redis/v9 v9.0.4
|
||||
golang.org/x/oauth2 v0.13.0
|
||||
k8s.io/api v0.29.3
|
||||
k8s.io/apimachinery v0.29.3
|
||||
k8s.io/client-go v0.29.3
|
||||
)
|
||||
|
||||
require (
|
||||
@ -21,60 +36,91 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/cli v20.10.17+incompatible // indirect
|
||||
github.com/docker/docker v20.10.13+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.5 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/qri-io/jsonpointer v0.1.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cdr.dev/slog v1.4.2 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.9.0 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-playground/locales v0.12.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.16.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/leodido/go-urn v1.1.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.9
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.19
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lib/pq v1.10.0 // indirect
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0
|
||||
@ -86,11 +132,11 @@ require (
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/mod v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.7.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
197
go.sum
197
go.sum
@ -49,8 +49,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333 h1:dAajr9wX8WuFPrwjbKNXRmbF+4AaAT7bUj66G7gdZ+c=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230512083245-e2dc3e1a0333/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 h1:FTk0ZoaV5N8Tkps5Da5RrDMZZXSHZIuD67Hy1Y4fsos=
|
||||
forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
@ -80,8 +80,14 @@ github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUS
|
||||
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
|
||||
github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||
@ -126,6 +132,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
|
||||
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
@ -140,10 +148,12 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
|
||||
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
@ -160,6 +170,10 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
|
||||
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@ -169,6 +183,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/expr-lang/expr v1.16.7 h1:gCIiHt5ODA0xIaDbD0DPKyZpM9Drph3b3lolYAYq2Kw=
|
||||
github.com/expr-lang/expr v1.16.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
@ -176,20 +192,39 @@ github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBD
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/getsentry/sentry-go v0.22.0 h1:XNX9zKbv7baSEI65l+H1GEJgSeIC1c7EN5kluWaP6dM=
|
||||
github.com/getsentry/sentry-go v0.22.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@ -228,12 +263,15 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@ -247,8 +285,12 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@ -266,6 +308,7 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
@ -282,7 +325,11 @@ github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAk
|
||||
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@ -299,7 +346,11 @@ github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1R
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@ -307,31 +358,33 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.19 h1:ekv1qEZE6BVct89QA+pRF6+4pCpfVrOnEJnTnT4RXoY=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.19/go.mod h1:l3im3coce1lL2cDeAjqmaR+Awx+X8Ih+2k8BuHNJ4CU=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
|
||||
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@ -347,6 +400,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@ -357,17 +412,31 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
|
||||
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
@ -379,6 +448,9 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
|
||||
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
|
||||
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@ -386,7 +458,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
|
||||
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
|
||||
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
|
||||
@ -397,24 +477,28 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@ -428,8 +512,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
@ -475,9 +559,8 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -514,9 +597,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -559,9 +641,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -578,6 +659,8 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
||||
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -590,8 +673,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -657,21 +740,19 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -681,13 +762,15 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@ -743,9 +826,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -789,6 +871,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@ -889,8 +973,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@ -901,11 +986,14 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
@ -914,6 +1002,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
|
||||
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@ -921,6 +1010,24 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
|
||||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
||||
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
|
||||
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
|
||||
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
|
||||
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
|
||||
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
|
122
internal/admin/bootstrap.go
Normal file
122
internal/admin/bootstrap.go
Normal file
@ -0,0 +1,122 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock/redis"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (s *Server) bootstrapProxies(ctx context.Context) error {
|
||||
if err := s.validateBootstrap(ctx); err != nil {
|
||||
return errors.Wrap(err, "could not validate bootstrapped proxies")
|
||||
}
|
||||
|
||||
proxyRepo := s.proxyRepository
|
||||
layerRepo := s.layerRepository
|
||||
|
||||
lockTimeout := time.Duration(s.bootstrapConfig.LockTimeout)
|
||||
locker := redis.NewLocker(s.redisClient, int(s.bootstrapConfig.MaxConnectionRetries))
|
||||
|
||||
err := locker.WithLock(ctx, "bouncer-admin-bootstrap", lockTimeout, func(ctx context.Context) error {
|
||||
logger.Info(ctx, "bootstrapping proxies")
|
||||
|
||||
for proxyName, proxyConfig := range s.bootstrapConfig.Proxies {
|
||||
loopCtx := logger.With(ctx, logger.F("proxyName", proxyName), logger.F("proxyFrom", proxyConfig.From), logger.F("proxyTo", proxyConfig.To))
|
||||
|
||||
_, err := s.proxyRepository.GetProxy(ctx, proxyName)
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if proxyConfig.Recreate {
|
||||
logger.Info(loopCtx, "force recreating proxy")
|
||||
|
||||
if err := s.deleteProxyAndLayers(ctx, proxyName); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
logger.Info(loopCtx, "ignoring existing proxy")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info(loopCtx, "creating proxy")
|
||||
|
||||
if _, err := proxyRepo.CreateProxy(ctx, proxyName, string(proxyConfig.To), proxyConfig.From...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err = proxyRepo.UpdateProxy(
|
||||
ctx, proxyName,
|
||||
store.WithProxyUpdateEnabled(bool(proxyConfig.Enabled)),
|
||||
store.WithProxyUpdateWeight(int(proxyConfig.Weight)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for layerName, layerConfig := range proxyConfig.Layers {
|
||||
layerType := store.LayerType(layerConfig.Type)
|
||||
layerOptions := store.LayerOptions(layerConfig.Options)
|
||||
|
||||
if _, err := layerRepo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err := layerRepo.UpdateLayer(
|
||||
ctx,
|
||||
proxyName, layerName,
|
||||
store.WithLayerUpdateEnabled(bool(layerConfig.Enabled)),
|
||||
store.WithLayerUpdateOptions(layerOptions),
|
||||
store.WithLayerUpdateWeight(int(layerConfig.Weight)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const validateErrMessage = "could not validate proxy '%s': could not validate layer '%s'"
|
||||
|
||||
func (s *Server) validateBootstrap(ctx context.Context) error {
|
||||
for proxyName, proxyConf := range s.bootstrapConfig.Proxies {
|
||||
for layerName, layerConf := range proxyConf.Layers {
|
||||
layerType := store.LayerType(layerConf.Type)
|
||||
if !setup.LayerTypeExists(layerType) {
|
||||
return errors.Errorf(validateErrMessage+": could not find layer type '%s'", proxyName, layerName, layerType)
|
||||
}
|
||||
|
||||
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layerType)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, validateErrMessage, proxyName, layerName)
|
||||
}
|
||||
|
||||
rawOptions := func(opts config.InterpolatedMap) map[string]any {
|
||||
return opts
|
||||
}(layerConf.Options)
|
||||
|
||||
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
|
||||
return errors.Wrapf(err, validateErrMessage, proxyName, layerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const ErrCodeAlreadyExist api.ErrorCode = "already-exist"
|
||||
@ -29,3 +32,8 @@ func invalidDataErrorResponse(w http.ResponseWriter, r *http.Request, err *schem
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func logAndCaptureError(ctx context.Context, message string, err error) {
|
||||
sentry.CaptureException(err)
|
||||
logger.Error(ctx, message, logger.E(err))
|
||||
}
|
||||
|
@ -3,11 +3,18 @@ package admin
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (s *Server) initRepositories(ctx context.Context) error {
|
||||
if err := s.initRedisClient(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := s.initLayerRepository(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -19,8 +26,16 @@ func (s *Server) initRepositories(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) initRedisClient(ctx context.Context) error {
|
||||
client := setup.NewRedisClient(ctx, s.redisConfig)
|
||||
|
||||
s.redisClient = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) initLayerRepository(ctx context.Context) error {
|
||||
layerRepository, err := setup.NewLayerRepository(ctx, s.redisConfig)
|
||||
layerRepository, err := setup.NewLayerRepository(ctx, s.redisClient)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -31,7 +46,7 @@ func (s *Server) initLayerRepository(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *Server) initProxyRepository(ctx context.Context) error {
|
||||
proxyRepository, err := setup.NewProxyRepository(ctx, s.redisConfig)
|
||||
proxyRepository, err := setup.NewProxyRepository(ctx, s.redisClient)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -40,3 +55,34 @@ func (s *Server) initProxyRepository(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) initPrivateKey(ctx context.Context) error {
|
||||
localKey, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
ctx = integration.WithPrivateKey(ctx, localKey)
|
||||
|
||||
key, err := integration.RunOnKeyLoad(ctx, s.integrations)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
s.privateKey = key
|
||||
} else {
|
||||
s.privateKey = localKey
|
||||
}
|
||||
|
||||
logger.Info(ctx, "using private key", logger.F("keyID", s.privateKey.KeyID()))
|
||||
|
||||
publicKeys, err := jwk.PublicKeySet(s.privateKey)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.publicKeys = publicKeys
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
@ -10,7 +11,6 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type QueryLayerResponse struct {
|
||||
@ -38,7 +38,7 @@ func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) {
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not list layers", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not list layers", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -51,15 +51,6 @@ func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func validateLayerName(v string) (store.LayerName, error) {
|
||||
name, err := store.ValidateName(v)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return store.LayerName(name), nil
|
||||
}
|
||||
|
||||
type GetLayerResponse struct {
|
||||
Layer *store.Layer `json:"layer"`
|
||||
}
|
||||
@ -85,7 +76,7 @@ func (s *Server) getLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -120,7 +111,7 @@ func (s *Server) deleteLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not delete layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not delete layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -156,7 +147,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
layerName, err := store.ValidateName(createLayerReq.Name)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "invalid 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "invalid 'name' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||
|
||||
return
|
||||
@ -165,7 +156,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
layerType := store.LayerType(createLayerReq.Type)
|
||||
|
||||
if !setup.LayerTypeExists(layerType) {
|
||||
logger.Error(r.Context(), "unknown layer type", logger.E(errors.WithStack(err)), logger.F("layerType", layerType))
|
||||
logAndCaptureError(ctx, fmt.Sprintf("unknown layer type '%s'", layerType), errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||
|
||||
return
|
||||
@ -179,7 +170,7 @@ func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not create layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not create layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -223,7 +214,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not get layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -247,7 +238,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
if updateLayerReq.Options != nil {
|
||||
layerOptionsSchema, err := setup.GetLayerOptionsSchema(layer.Type)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not retrieve layer options schema", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not retrieve layer options schema", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -258,7 +249,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
}(updateLayerReq.Options)
|
||||
|
||||
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {
|
||||
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not validate layer options", errors.WithStack(err))
|
||||
|
||||
var invalidDataErr *schema.InvalidDataError
|
||||
if errors.As(err, &invalidDataErr) {
|
||||
@ -286,7 +277,7 @@ func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not update layer", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not update layer", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -300,21 +291,7 @@ func getLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool
|
||||
|
||||
name, err := store.ValidateName(rawLayerName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return store.LayerName(name), true
|
||||
}
|
||||
|
||||
func geLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) {
|
||||
rawLayerName := chi.URLParam(r, "layerName")
|
||||
|
||||
name, err := store.ValidateName(rawLayerName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse layer name", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
|
@ -2,11 +2,14 @@ package admin
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
ServerConfig config.AdminServerConfig
|
||||
RedisConfig config.RedisConfig
|
||||
BootstrapConfig config.BootstrapConfig
|
||||
ServerConfig config.AdminServerConfig
|
||||
RedisConfig config.RedisConfig
|
||||
Integrations []integration.Integration
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
@ -15,6 +18,7 @@ func defaultOption() *Option {
|
||||
return &Option{
|
||||
ServerConfig: config.NewDefaultAdminServerConfig(),
|
||||
RedisConfig: config.NewDefaultRedisConfig(),
|
||||
Integrations: make([]integration.Integration, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,3 +33,15 @@ func WithRedisConfig(conf config.RedisConfig) OptionFunc {
|
||||
opt.RedisConfig = conf
|
||||
}
|
||||
}
|
||||
|
||||
func WithBootstrapConfig(conf config.BootstrapConfig) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.BootstrapConfig = conf
|
||||
}
|
||||
}
|
||||
|
||||
func WithIntegrations(integrations ...integration.Integration) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Integrations = integrations
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type QueryProxyResponse struct {
|
||||
@ -37,7 +36,7 @@ func (s *Server) queryProxy(w http.ResponseWriter, r *http.Request) {
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not list proxies", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not list proxies", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -79,7 +78,7 @@ func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not get proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -102,14 +101,14 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
if err := s.proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
|
||||
if err := s.deleteProxyAndLayers(ctx, proxyName); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not delete proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not delete proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -140,14 +139,14 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
name, err := store.ValidateName(createProxyReq.Name)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not parse 'name' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := url.Parse(createProxyReq.To); err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
@ -161,7 +160,7 @@ func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not create proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not create proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -207,7 +206,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
if updateProxyReq.To != nil {
|
||||
_, err := url.Parse(*updateProxyReq.To)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
@ -235,7 +234,7 @@ func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not update proxy", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(ctx, "could not update proxy", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
@ -249,7 +248,7 @@ func getProxyName(w http.ResponseWriter, r *http.Request) (store.ProxyName, bool
|
||||
|
||||
name, err := store.ValidateName(rawProxyName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse proxy name", logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse proxy name", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
@ -263,7 +262,7 @@ func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defa
|
||||
if rawValue != "" {
|
||||
value, err := strconv.ParseInt(rawValue, 10, 64)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse int param", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
@ -296,7 +295,7 @@ func getStringableSliceValues[T ~string](w http.ResponseWriter, r *http.Request,
|
||||
for _, rv := range rawValues {
|
||||
v, err := validate(rv)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse ids slice param", logger.F("param", param), logger.E(errors.WithStack(err)))
|
||||
logAndCaptureError(r.Context(), "could not parse ids slice param", errors.WithStack(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return nil, false
|
||||
|
@ -9,21 +9,35 @@ import (
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
serverConfig config.AdminServerConfig
|
||||
redisConfig config.RedisConfig
|
||||
serverConfig config.AdminServerConfig
|
||||
redisConfig config.RedisConfig
|
||||
|
||||
redisClient redis.UniversalClient
|
||||
|
||||
integrations []integration.Integration
|
||||
|
||||
bootstrapConfig config.BootstrapConfig
|
||||
proxyRepository store.ProxyRepository
|
||||
layerRepository store.LayerRepository
|
||||
|
||||
privateKey jwk.Key
|
||||
publicKeys jwk.Set
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||
@ -50,6 +64,27 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.bootstrapProxies(ctx); err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.initPrivateKey(ctx); err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = integration.WithPrivateKey(ctx, s.privateKey)
|
||||
ctx = integration.WithPublicKeySet(ctx, s.publicKeys)
|
||||
|
||||
if err := integration.RunOnStartup(ctx, s.integrations); err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.serverConfig.HTTP.Host, s.serverConfig.HTTP.Port))
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
@ -73,23 +108,23 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
}
|
||||
}()
|
||||
|
||||
key, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := jwk.PublicKeySet(key)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.Logger)
|
||||
if s.serverConfig.HTTP.UseRealIP {
|
||||
router.Use(middleware.RealIP)
|
||||
}
|
||||
|
||||
router.Use(middleware.RequestLogger(bouncerChi.NewLogFormatter()))
|
||||
|
||||
if s.serverConfig.Sentry.DSN != "" {
|
||||
logger.Info(ctx, "enabling sentry http middleware")
|
||||
|
||||
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: true,
|
||||
})
|
||||
|
||||
router.Use(sentryMiddleware.Handle)
|
||||
}
|
||||
|
||||
corsMiddleware := cors.New(cors.Options{
|
||||
AllowedOrigins: s.serverConfig.CORS.AllowedOrigins,
|
||||
@ -101,10 +136,29 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
|
||||
router.Use(corsMiddleware.Handler)
|
||||
|
||||
if s.serverConfig.Metrics.Enabled {
|
||||
metrics := s.serverConfig.Metrics
|
||||
|
||||
logger.Info(ctx, "enabling metrics", logger.F("endpoint", metrics.Endpoint))
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
if metrics.BasicAuth != nil {
|
||||
logger.Info(ctx, "enabling authentication on metrics endpoint")
|
||||
|
||||
r.Use(middleware.BasicAuth(
|
||||
"metrics",
|
||||
metrics.BasicAuth.CredentialsMap(),
|
||||
))
|
||||
}
|
||||
|
||||
r.Handle(string(metrics.Endpoint), promhttp.Handler())
|
||||
})
|
||||
}
|
||||
|
||||
router.Route("/api/v1", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
jwt.NewAuthenticator(keys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew),
|
||||
jwt.NewAuthenticator(s.publicKeys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew),
|
||||
))
|
||||
|
||||
r.Route("/proxies", func(r chi.Router) {
|
||||
@ -139,7 +193,9 @@ func NewServer(funcs ...OptionFunc) *Server {
|
||||
}
|
||||
|
||||
return &Server{
|
||||
serverConfig: opt.ServerConfig,
|
||||
redisConfig: opt.RedisConfig,
|
||||
serverConfig: opt.ServerConfig,
|
||||
redisConfig: opt.RedisConfig,
|
||||
bootstrapConfig: opt.BootstrapConfig,
|
||||
integrations: opt.Integrations,
|
||||
}
|
||||
}
|
||||
|
29
internal/admin/util.go
Normal file
29
internal/admin/util.go
Normal file
@ -0,0 +1,29 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) deleteProxyAndLayers(ctx context.Context, proxyName store.ProxyName) error {
|
||||
if err := s.proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
layers, err := s.layerRepository.QueryLayers(ctx, proxyName)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
if err := s.layerRepository.DeleteLayer(ctx, proxyName, layer.Name); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -16,6 +16,7 @@ const keyRole = "role"
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string, acceptableSkew time.Duration) (jwt.Token, error) {
|
||||
token, err := jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithContext(ctx),
|
||||
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
||||
jwt.WithIssuer(issuer),
|
||||
jwt.WithValidate(true),
|
||||
@ -60,3 +61,17 @@ func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, rol
|
||||
|
||||
return string(rawToken), nil
|
||||
}
|
||||
|
||||
func GenerateTokenWithPrivateKey(ctx context.Context, privateKeyFile string, issuer string, subject string, role Role) (string, jwk.Key, error) {
|
||||
key, err := jwk.LoadOrGenerate(privateKeyFile, jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
return "", nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := GenerateToken(ctx, key, issuer, subject, role)
|
||||
if err != nil {
|
||||
return "", nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return token, key, nil
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ type LogFormatter struct{}
|
||||
func (*LogFormatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
return &LogEntry{
|
||||
method: r.Method,
|
||||
host: r.Host,
|
||||
path: r.URL.Path,
|
||||
ctx: r.Context(),
|
||||
}
|
||||
@ -29,6 +30,7 @@ var _ middleware.LogFormatter = &LogFormatter{}
|
||||
|
||||
type LogEntry struct {
|
||||
method string
|
||||
host string
|
||||
path string
|
||||
ctx context.Context
|
||||
}
|
||||
@ -41,6 +43,7 @@ func (e *LogEntry) Panic(v interface{}, stack []byte) {
|
||||
// Write implements middleware.LogEntry
|
||||
func (e *LogEntry) Write(status int, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
|
||||
logger.Info(e.ctx, fmt.Sprintf("%s %s - %d", e.method, e.path, status),
|
||||
logger.F("host", e.host),
|
||||
logger.F("status", status),
|
||||
logger.F("bytes", bytes),
|
||||
logger.F("elapsed", elapsed),
|
||||
|
@ -19,7 +19,7 @@ func CreateCommand() *cli.Command {
|
||||
Name: "create",
|
||||
Usage: "Create proxy",
|
||||
Flags: proxyFlag.WithProxyFlags(
|
||||
flag.ProxyTo(),
|
||||
flag.ProxyTo(true),
|
||||
flag.ProxyFrom(),
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
|
@ -30,12 +30,12 @@ func ProxyName() cli.Flag {
|
||||
|
||||
const KeyProxyTo = "proxy-to"
|
||||
|
||||
func ProxyTo() cli.Flag {
|
||||
func ProxyTo(required bool) cli.Flag {
|
||||
return &cli.StringFlag{
|
||||
Name: KeyProxyTo,
|
||||
Usage: "Set `PROXY_TO` as proxy's destination url",
|
||||
Value: "",
|
||||
Required: true,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ func UpdateCommand() *cli.Command {
|
||||
Name: "update",
|
||||
Usage: "Update proxy",
|
||||
Flags: proxyFlag.WithProxyFlags(
|
||||
flag.ProxyTo(),
|
||||
flag.ProxyTo(false),
|
||||
flag.ProxyFrom(),
|
||||
flag.ProxyEnabled(),
|
||||
flag.ProxyWeight(),
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
@ -30,20 +29,15 @@ func CreateTokenCommand() *cli.Command {
|
||||
Action: func(ctx *cli.Context) error {
|
||||
conf, err := common.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
return errors.Wrap(err, "could not load configuration")
|
||||
}
|
||||
|
||||
subject := ctx.String("subject")
|
||||
role := ctx.String("role")
|
||||
|
||||
key, err := jwk.LoadOrGenerate(string(conf.Admin.Auth.PrivateKey), jwk.DefaultKeySize)
|
||||
token, _, err := jwt.GenerateTokenWithPrivateKey(ctx.Context, string(conf.Admin.Auth.PrivateKey), string(conf.Admin.Auth.Issuer), subject, jwt.Role(role))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := jwt.GenerateToken(ctx.Context, key, string(conf.Admin.Auth.Issuer), subject, jwt.Role(role))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return errors.Wrap(err, "could not generate token")
|
||||
}
|
||||
|
||||
fmt.Println(token)
|
||||
|
@ -18,6 +18,8 @@ func Dump() *cli.Command {
|
||||
Usage: "Dump the current configuration",
|
||||
Flags: flags,
|
||||
Action: func(ctx *cli.Context) error {
|
||||
logger.SetLevel(logger.LevelError)
|
||||
|
||||
conf, err := common.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -50,9 +51,10 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "workdir",
|
||||
Value: "",
|
||||
Usage: "The working directory",
|
||||
Name: "workdir",
|
||||
Value: "",
|
||||
EnvVars: []string{"BOUNCER_WORKDIR"},
|
||||
Usage: "The working directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "projectVersion",
|
||||
@ -89,6 +91,8 @@ func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands
|
||||
return
|
||||
}
|
||||
|
||||
sentry.CaptureException(err)
|
||||
|
||||
debug := ctx.Bool("debug")
|
||||
|
||||
if !debug {
|
||||
|
@ -6,13 +6,20 @@ import (
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/admin"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
flagPrintDefaultToken = "print-default-token"
|
||||
)
|
||||
|
||||
func RunCommand() *cli.Command {
|
||||
flags := common.Flags()
|
||||
flags := append(
|
||||
common.Flags(),
|
||||
)
|
||||
|
||||
return &cli.Command{
|
||||
Name: "run",
|
||||
@ -27,9 +34,24 @@ func RunCommand() *cli.Command {
|
||||
logger.SetFormat(logger.Format(conf.Logger.Format))
|
||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||
|
||||
projectVersion := ctx.String("projectVersion")
|
||||
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Admin.Sentry, projectVersion)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize sentry client")
|
||||
}
|
||||
|
||||
defer flushSentry()
|
||||
|
||||
integrations, err := setup.SetupIntegrations(ctx.Context, conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not setup integrations")
|
||||
}
|
||||
|
||||
srv := admin.NewServer(
|
||||
admin.WithServerConfig(conf.Admin),
|
||||
admin.WithRedisConfig(conf.Redis),
|
||||
admin.WithBootstrapConfig(conf.Bootstrap),
|
||||
admin.WithIntegrations(integrations...),
|
||||
)
|
||||
|
||||
addrs, srvErrs := srv.Start(ctx.Context)
|
||||
|
65
internal/command/server/dummy/index.gohtml
Normal file
65
internal/command/server/dummy/index.gohtml
Normal file
@ -0,0 +1,65 @@
|
||||
<html>
|
||||
<body>
|
||||
<h1>Received request</h1>
|
||||
<h2>Incoming headers</h2>
|
||||
<table style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $key, $val := .Request.Header }}
|
||||
<tr>
|
||||
<td>
|
||||
<b>{{ $key }}</b>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ $val }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{
|
||||
end
|
||||
}}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Incoming cookies</h2>
|
||||
<table style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Domain</th>
|
||||
<th>Path</th>
|
||||
<th>Secure</th>
|
||||
<th>MaxAge</th>
|
||||
<th>HttpOnly</th>
|
||||
<th>SameSite</th>
|
||||
<th>Expires</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $cookie := .Request.Cookies }}
|
||||
<tr>
|
||||
<td>
|
||||
<b>{{ $cookie.Name }}</b>
|
||||
</td>
|
||||
<td>{{ $cookie.Domain }}</td>
|
||||
<td>{{ $cookie.Path }}</td>
|
||||
<td>{{ $cookie.Secure }}</td>
|
||||
<td>{{ $cookie.MaxAge }}</td>
|
||||
<td>{{ $cookie.HttpOnly }}</td>
|
||||
<td>{{ $cookie.SameSite }}</td>
|
||||
<td>{{ $cookie.Expires }}</td>
|
||||
<td>
|
||||
<code>{{ $cookie.Value }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{
|
||||
end
|
||||
}}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
15
internal/command/server/dummy/root.go
Normal file
15
internal/command/server/dummy/root.go
Normal file
@ -0,0 +1,15 @@
|
||||
package dummy
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Root() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "dummy",
|
||||
Usage: "Dummy server related commands",
|
||||
Subcommands: []*cli.Command{
|
||||
RunCommand(),
|
||||
},
|
||||
}
|
||||
}
|
69
internal/command/server/dummy/run.go
Normal file
69
internal/command/server/dummy/run.go
Normal file
@ -0,0 +1,69 @@
|
||||
package dummy
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/common"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed index.gohtml
|
||||
indexTmpl string
|
||||
)
|
||||
|
||||
func RunCommand() *cli.Command {
|
||||
flags := common.Flags()
|
||||
|
||||
return &cli.Command{
|
||||
Name: "run",
|
||||
Usage: "Run the dummy server",
|
||||
Description: "The dummy server is a very basic web application allowing the debug of incoming requests",
|
||||
Flags: append(flags, &cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "the dummy server listening address",
|
||||
Value: ":8082",
|
||||
}),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
address := ctx.String("address")
|
||||
|
||||
conf, err := common.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load configuration")
|
||||
}
|
||||
|
||||
logger.SetFormat(logger.Format(conf.Logger.Format))
|
||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||
|
||||
tmpl, err := template.New("").Parse(indexTmpl)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := struct {
|
||||
Request *http.Request
|
||||
}{
|
||||
Request: r,
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
logger.Error(ctx.Context, "could not execute template", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
})
|
||||
|
||||
logger.Info(ctx.Context, "listening", logger.F("address", address))
|
||||
|
||||
if err := http.ListenAndServe(address, handler); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
@ -28,6 +28,14 @@ func RunCommand() *cli.Command {
|
||||
logger.SetFormat(logger.Format(conf.Logger.Format))
|
||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||
|
||||
projectVersion := ctx.String("projectVersion")
|
||||
flushSentry, err := setup.SetupSentry(ctx.Context, conf.Proxy.Sentry, projectVersion)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize sentry client")
|
||||
}
|
||||
|
||||
defer flushSentry()
|
||||
|
||||
layers, err := setup.GetLayers(ctx.Context, conf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize director layers")
|
||||
|
@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/server/admin"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/server/dummy"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/command/server/proxy"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -13,6 +14,7 @@ func Root() *cli.Command {
|
||||
Subcommands: []*cli.Command{
|
||||
proxy.Root(),
|
||||
admin.Root(),
|
||||
dummy.Root(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,20 @@
|
||||
package config
|
||||
|
||||
type AdminServerConfig struct {
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
Metrics MetricsConfig `yaml:"metrics"`
|
||||
Sentry SentryConfig `yaml:"sentry"`
|
||||
}
|
||||
|
||||
func NewDefaultAdminServerConfig() AdminServerConfig {
|
||||
return AdminServerConfig{
|
||||
HTTP: NewHTTPConfig("127.0.0.1", 8081),
|
||||
CORS: NewDefaultCORSConfig(),
|
||||
Auth: NewDefaultAuthConfig(),
|
||||
HTTP: NewHTTPConfig("127.0.0.1", 8081),
|
||||
CORS: NewDefaultCORSConfig(),
|
||||
Auth: NewDefaultAuthConfig(),
|
||||
Metrics: NewDefaultMetricsConfig(),
|
||||
Sentry: NewDefaultSentryConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
107
internal/config/bootstrap.go
Normal file
107
internal/config/bootstrap.go
Normal file
@ -0,0 +1,107 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type BootstrapConfig struct {
|
||||
Proxies map[store.ProxyName]BootstrapProxyConfig `yaml:"proxies"`
|
||||
Dir InterpolatedString `yaml:"dir"`
|
||||
LockTimeout InterpolatedDuration `yaml:"lockTimeout"`
|
||||
MaxConnectionRetries InterpolatedInt `yaml:"maxRetries"`
|
||||
}
|
||||
|
||||
func (c *BootstrapConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
src := struct {
|
||||
Proxies map[store.ProxyName]BootstrapProxyConfig `yaml:"proxies"`
|
||||
Dir InterpolatedString `yaml:"dir"`
|
||||
}{
|
||||
Proxies: make(map[store.ProxyName]BootstrapProxyConfig),
|
||||
Dir: "",
|
||||
}
|
||||
|
||||
if err := unmarshal(&src); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.Proxies = src.Proxies
|
||||
c.Dir = src.Dir
|
||||
|
||||
if src.Dir != "" {
|
||||
proxies, err := loadBootstrapDir(string(src.Dir))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not load bootstrap dir '%s'", src.Dir)
|
||||
}
|
||||
|
||||
c.Proxies = overrideProxies(c.Proxies, proxies)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type BootstrapProxyConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
Weight InterpolatedInt `yaml:"weight"`
|
||||
To InterpolatedString `yaml:"to"`
|
||||
From InterpolatedStringSlice `yaml:"from"`
|
||||
Layers map[store.LayerName]BootstrapLayerConfig `yaml:"layers"`
|
||||
Recreate InterpolatedBool `yaml:"recreate"`
|
||||
}
|
||||
|
||||
type BootstrapLayerConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
Type InterpolatedString `yaml:"type"`
|
||||
Weight InterpolatedInt `yaml:"weight"`
|
||||
Options InterpolatedMap `yaml:"options"`
|
||||
}
|
||||
|
||||
func NewDefaultBootstrapConfig() BootstrapConfig {
|
||||
return BootstrapConfig{
|
||||
Dir: "",
|
||||
LockTimeout: *NewInterpolatedDuration(30 * time.Second),
|
||||
MaxConnectionRetries: 10,
|
||||
}
|
||||
}
|
||||
|
||||
func loadBootstrapDir(dir string) (map[store.ProxyName]BootstrapProxyConfig, error) {
|
||||
pattern := filepath.Join(dir, "*.yml")
|
||||
|
||||
files, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
proxies := make(map[store.ProxyName]BootstrapProxyConfig)
|
||||
for _, f := range files {
|
||||
data, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read file '%s'", f)
|
||||
}
|
||||
|
||||
proxy := BootstrapProxyConfig{}
|
||||
|
||||
if err := yaml.Unmarshal(data, &proxy); err != nil {
|
||||
return nil, errors.Wrapf(err, "could not unmarshal proxy")
|
||||
}
|
||||
|
||||
name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
|
||||
proxies[name] = proxy
|
||||
}
|
||||
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
func overrideProxies(base map[store.ProxyName]BootstrapProxyConfig, proxies map[store.ProxyName]BootstrapProxyConfig) map[store.ProxyName]BootstrapProxyConfig {
|
||||
for name, proxy := range proxies {
|
||||
base[name] = proxy
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
@ -2,7 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -10,18 +10,20 @@ import (
|
||||
|
||||
// Config definition
|
||||
type Config struct {
|
||||
Admin AdminServerConfig `yaml:"admin"`
|
||||
Proxy ProxyServerConfig `yaml:"proxy"`
|
||||
Redis RedisConfig `yaml:"redis"`
|
||||
Logger LoggerConfig `yaml:"logger"`
|
||||
Layers LayersConfig `yaml:"layers"`
|
||||
Admin AdminServerConfig `yaml:"admin"`
|
||||
Proxy ProxyServerConfig `yaml:"proxy"`
|
||||
Redis RedisConfig `yaml:"redis"`
|
||||
Logger LoggerConfig `yaml:"logger"`
|
||||
Layers LayersConfig `yaml:"layers"`
|
||||
Bootstrap BootstrapConfig `yaml:"bootstrap"`
|
||||
Integrations IntegrationsConfig `yaml:"integrations"`
|
||||
}
|
||||
|
||||
// NewFromFile retrieves the configuration from the given file
|
||||
func NewFromFile(path string) (*Config, error) {
|
||||
config := NewDefault()
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read file '%s'", path)
|
||||
}
|
||||
@ -43,11 +45,13 @@ func NewDumpDefault() *Config {
|
||||
// NewDefault return new default configuration
|
||||
func NewDefault() *Config {
|
||||
return &Config{
|
||||
Admin: NewDefaultAdminServerConfig(),
|
||||
Proxy: NewDefaultProxyServerConfig(),
|
||||
Logger: NewDefaultLoggerConfig(),
|
||||
Redis: NewDefaultRedisConfig(),
|
||||
Layers: NewDefaultLayersConfig(),
|
||||
Admin: NewDefaultAdminServerConfig(),
|
||||
Proxy: NewDefaultProxyServerConfig(),
|
||||
Logger: NewDefaultLoggerConfig(),
|
||||
Redis: NewDefaultRedisConfig(),
|
||||
Layers: NewDefaultLayersConfig(),
|
||||
Bootstrap: NewDefaultBootstrapConfig(),
|
||||
Integrations: NewDefaultIntegrationsConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,13 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/drone/envsubst"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var reVar = regexp.MustCompile(`^\${(\w+)}$`)
|
||||
// var reVar = regexp.MustCompile(`^\${(\w+)}$`)
|
||||
var reVar = regexp.MustCompile(`\${(.*?)}`)
|
||||
|
||||
type InterpolatedString string
|
||||
|
||||
@ -53,6 +55,29 @@ func (ii *InterpolatedInt) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type InterpolatedFloat float64
|
||||
|
||||
func (ifl *InterpolatedFloat) UnmarshalYAML(value *yaml.Node) error {
|
||||
var str string
|
||||
|
||||
if err := value.Decode(&str); err != nil {
|
||||
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line)
|
||||
}
|
||||
|
||||
if match := reVar.FindStringSubmatch(str); len(match) > 0 {
|
||||
str = os.Getenv(match[1])
|
||||
}
|
||||
|
||||
floatVal, err := strconv.ParseFloat(str, 10)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not parse float '%v', line '%d'", str, value.Line)
|
||||
}
|
||||
|
||||
*ifl = InterpolatedFloat(floatVal)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type InterpolatedBool bool
|
||||
|
||||
func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error {
|
||||
@ -107,14 +132,22 @@ type InterpolatedStringSlice []string
|
||||
|
||||
func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error {
|
||||
var data []string
|
||||
var evErr error
|
||||
|
||||
if err := value.Decode(&data); err != nil {
|
||||
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line)
|
||||
}
|
||||
|
||||
for index, value := range data {
|
||||
if match := reVar.FindStringSubmatch(value); len(match) > 0 {
|
||||
value = os.Getenv(match[1])
|
||||
//match := reVar.FindStringSubmatch(value)
|
||||
re := regexp.MustCompile(`\${(.*?)}`)
|
||||
|
||||
res := re.FindAllStringSubmatch(value, 10)
|
||||
if len(res) > 0 {
|
||||
value, evErr = envsubst.EvalEnv(value)
|
||||
if evErr != nil {
|
||||
return evErr
|
||||
}
|
||||
}
|
||||
|
||||
data[index] = value
|
||||
|
@ -1,13 +1,15 @@
|
||||
package config
|
||||
|
||||
type HTTPConfig struct {
|
||||
Host InterpolatedString `yaml:"host"`
|
||||
Port InterpolatedInt `yaml:"port"`
|
||||
Host InterpolatedString `yaml:"host"`
|
||||
Port InterpolatedInt `yaml:"port"`
|
||||
UseRealIP InterpolatedBool `yaml:"useRealIP"`
|
||||
}
|
||||
|
||||
func NewHTTPConfig(host string, port int) HTTPConfig {
|
||||
return HTTPConfig{
|
||||
Host: InterpolatedString(host),
|
||||
Port: InterpolatedInt(port),
|
||||
Host: InterpolatedString(host),
|
||||
Port: InterpolatedInt(port),
|
||||
UseRealIP: true,
|
||||
}
|
||||
}
|
||||
|
33
internal/config/integrations.go
Normal file
33
internal/config/integrations.go
Normal file
@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type IntegrationsConfig struct {
|
||||
Kubernetes KubernetesConfig `yaml:"kubernetes"`
|
||||
}
|
||||
|
||||
func NewDefaultIntegrationsConfig() IntegrationsConfig {
|
||||
return IntegrationsConfig{
|
||||
Kubernetes: KubernetesConfig{
|
||||
Enabled: false,
|
||||
WriterTokenSecret: "",
|
||||
WriterTokenSecretNamespace: "",
|
||||
ReaderTokenSecretNamespace: "",
|
||||
PrivateKeySecret: "",
|
||||
PrivateKeySecretNamespace: "",
|
||||
ReaderTokenSecret: "",
|
||||
LockTimeout: *NewInterpolatedDuration(30 * time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type KubernetesConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
WriterTokenSecret InterpolatedString `yaml:"writerTokenSecret"`
|
||||
WriterTokenSecretNamespace InterpolatedString `yaml:"writerTokenSecretNamespace"`
|
||||
ReaderTokenSecret InterpolatedString `yaml:"readerTokenSecret"`
|
||||
ReaderTokenSecretNamespace InterpolatedString `yaml:"readerTokenSecretNamespace"`
|
||||
PrivateKeySecret InterpolatedString `yaml:"privateKeySecret"`
|
||||
PrivateKeySecretNamespace InterpolatedString `yaml:"privateKeySecretNamespace"`
|
||||
LockTimeout InterpolatedDuration `yaml:"lockTimeout"`
|
||||
}
|
@ -3,7 +3,8 @@ package config
|
||||
import "time"
|
||||
|
||||
type LayersConfig struct {
|
||||
Queue QueueLayerConfig `yaml:"queue"`
|
||||
Queue QueueLayerConfig `yaml:"queue"`
|
||||
CircuitBreaker CircuitBreakerLayerConfig `yaml:"circuitbreaker"`
|
||||
}
|
||||
|
||||
func NewDefaultLayersConfig() LayersConfig {
|
||||
@ -12,6 +13,9 @@ func NewDefaultLayersConfig() LayersConfig {
|
||||
TemplateDir: "./layers/queue/templates",
|
||||
DefaultKeepAlive: NewInterpolatedDuration(time.Minute),
|
||||
},
|
||||
CircuitBreaker: CircuitBreakerLayerConfig{
|
||||
TemplateDir: "./layers/circuitbreaker/templates",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,3 +23,7 @@ type QueueLayerConfig struct {
|
||||
TemplateDir InterpolatedString `yaml:"templateDir"`
|
||||
DefaultKeepAlive *InterpolatedDuration `yaml:"defaultKeepAlive"`
|
||||
}
|
||||
|
||||
type CircuitBreakerLayerConfig struct {
|
||||
TemplateDir InterpolatedString `yaml:"templateDir"`
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ type LoggerConfig struct {
|
||||
|
||||
func NewDefaultLoggerConfig() LoggerConfig {
|
||||
return LoggerConfig{
|
||||
Level: InterpolatedInt(logger.LevelInfo),
|
||||
Level: InterpolatedInt(logger.LevelError),
|
||||
Format: InterpolatedString(logger.FormatHuman),
|
||||
}
|
||||
}
|
||||
|
35
internal/config/metrics.go
Normal file
35
internal/config/metrics.go
Normal file
@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type MetricsConfig struct {
|
||||
Enabled InterpolatedBool `yaml:"enabled"`
|
||||
Endpoint InterpolatedString `yaml:"endpoint"`
|
||||
BasicAuth *BasicAuthConfig `yaml:"basicAuth"`
|
||||
}
|
||||
|
||||
type BasicAuthConfig struct {
|
||||
Credentials *InterpolatedMap `yaml:"credentials"`
|
||||
}
|
||||
|
||||
func (c *BasicAuthConfig) CredentialsMap() map[string]string {
|
||||
if c.Credentials == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
credentials := make(map[string]string, len(*c.Credentials))
|
||||
|
||||
for k, v := range *c.Credentials {
|
||||
credentials[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
return credentials
|
||||
}
|
||||
|
||||
func NewDefaultMetricsConfig() MetricsConfig {
|
||||
return MetricsConfig{
|
||||
Enabled: true,
|
||||
Endpoint: "/.bouncer/metrics",
|
||||
BasicAuth: nil,
|
||||
}
|
||||
}
|
@ -1,11 +1,73 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type ProxyServerConfig struct {
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
Metrics MetricsConfig `yaml:"metrics"`
|
||||
Transport TransportConfig `yaml:"transport"`
|
||||
Dial DialConfig `yaml:"dial"`
|
||||
Sentry SentryConfig `yaml:"sentry"`
|
||||
}
|
||||
|
||||
// See https://pkg.go.dev/net/http#Transport
|
||||
type TransportConfig struct {
|
||||
ForceAttemptHTTP2 InterpolatedBool `yaml:"forceAttemptHTTP2"`
|
||||
MaxIdleConns InterpolatedInt `yaml:"maxIdleConns"`
|
||||
MaxIdleConnsPerHost InterpolatedInt `yaml:"maxIdleConnsPerHost"`
|
||||
MaxConnsPerHost InterpolatedInt `yaml:"maxConnsPerHost"`
|
||||
IdleConnTimeout *InterpolatedDuration `yaml:"idleConnTimeout"`
|
||||
TLSHandshakeTimeout *InterpolatedDuration `yaml:"tlsHandshakeTimeout"`
|
||||
ExpectContinueTimeout *InterpolatedDuration `yaml:"expectContinueTimeout"`
|
||||
DisableKeepAlives InterpolatedBool `yaml:"disableKeepAlives"`
|
||||
DisableCompression InterpolatedBool `yaml:"disableCompression"`
|
||||
ResponseHeaderTimeout *InterpolatedDuration `yaml:"responseHeaderTimeout"`
|
||||
WriteBufferSize InterpolatedInt `yaml:"writeBufferSize"`
|
||||
ReadBufferSize InterpolatedInt `yaml:"readBufferSize"`
|
||||
MaxResponseHeaderBytes InterpolatedInt `yaml:"maxResponseHeaderBytes"`
|
||||
}
|
||||
|
||||
func NewDefaultProxyServerConfig() ProxyServerConfig {
|
||||
return ProxyServerConfig{
|
||||
HTTP: NewHTTPConfig("0.0.0.0", 8080),
|
||||
HTTP: NewHTTPConfig("0.0.0.0", 8080),
|
||||
Metrics: NewDefaultMetricsConfig(),
|
||||
Transport: NewDefaultTransportConfig(),
|
||||
Dial: NewDefaultDialConfig(),
|
||||
Sentry: NewDefaultSentryConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// See https://pkg.go.dev/net#Dialer
|
||||
type DialConfig struct {
|
||||
Timeout *InterpolatedDuration `yaml:"timeout"`
|
||||
KeepAlive *InterpolatedDuration `yaml:"keepAlive"`
|
||||
FallbackDelay *InterpolatedDuration `yaml:"fallbackDelay"`
|
||||
DualStack InterpolatedBool `yaml:"dualStack"`
|
||||
}
|
||||
|
||||
func NewDefaultDialConfig() DialConfig {
|
||||
return DialConfig{
|
||||
Timeout: NewInterpolatedDuration(30 * time.Second),
|
||||
KeepAlive: NewInterpolatedDuration(30 * time.Second),
|
||||
FallbackDelay: NewInterpolatedDuration(300 * time.Millisecond),
|
||||
DualStack: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultTransportConfig() TransportConfig {
|
||||
return TransportConfig{
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
MaxConnsPerHost: 100,
|
||||
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second),
|
||||
TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
|
||||
ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
|
||||
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
|
||||
DisableCompression: false,
|
||||
DisableKeepAlives: false,
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
MaxResponseHeaderBytes: 0,
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
RedisModeSimple = "simple"
|
||||
RedisModeSentinel = "sentinel"
|
||||
@ -7,13 +9,21 @@ const (
|
||||
)
|
||||
|
||||
type RedisConfig struct {
|
||||
Adresses InterpolatedStringSlice `yaml:"addresses"`
|
||||
Master InterpolatedString `yaml:"master"`
|
||||
Adresses InterpolatedStringSlice `yaml:"addresses"`
|
||||
Master InterpolatedString `yaml:"master"`
|
||||
ReadTimeout InterpolatedDuration `yaml:"readTimeout"`
|
||||
WriteTimeout InterpolatedDuration `yaml:"writeTimeout"`
|
||||
DialTimeout InterpolatedDuration `yaml:"dialTimeout"`
|
||||
LockMaxRetries InterpolatedInt `yaml:"lockMaxRetries"`
|
||||
}
|
||||
|
||||
func NewDefaultRedisConfig() RedisConfig {
|
||||
return RedisConfig{
|
||||
Adresses: InterpolatedStringSlice{"localhost:6379"},
|
||||
Master: "",
|
||||
Adresses: InterpolatedStringSlice{"localhost:6379"},
|
||||
Master: "",
|
||||
ReadTimeout: InterpolatedDuration(30 * time.Second),
|
||||
WriteTimeout: InterpolatedDuration(30 * time.Second),
|
||||
DialTimeout: InterpolatedDuration(30 * time.Second),
|
||||
LockMaxRetries: 10,
|
||||
}
|
||||
}
|
||||
|
43
internal/config/sentry.go
Normal file
43
internal/config/sentry.go
Normal file
@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Sentry configuration
|
||||
// See https://pkg.go.dev/github.com/getsentry/sentry-go?utm_source=godoc#ClientOptions
|
||||
type SentryConfig struct {
|
||||
DSN InterpolatedString `yaml:"dsn"`
|
||||
Debug InterpolatedBool `yaml:"debug"`
|
||||
FlushTimeout *InterpolatedDuration `yaml:"flushTimeout"`
|
||||
AttachStacktrace InterpolatedBool `yaml:"attachStacktrace"`
|
||||
SampleRate InterpolatedFloat `yaml:"sampleRate"`
|
||||
EnableTracing InterpolatedBool `yaml:"enableTracing"`
|
||||
TracesSampleRate InterpolatedFloat `yaml:"tracesSampleRate"`
|
||||
ProfilesSampleRate InterpolatedFloat `yaml:"profilesSampleRate"`
|
||||
IgnoreErrors InterpolatedStringSlice `yaml:"ignoreErrors"`
|
||||
SendDefaultPII InterpolatedBool `yaml:"sendDefaultPII"`
|
||||
ServerName InterpolatedString `yaml:"serverName"`
|
||||
Environment InterpolatedString `yaml:"environment"`
|
||||
MaxBreadcrumbs InterpolatedInt `yaml:"maxBreadcrumbs"`
|
||||
MaxSpans InterpolatedInt `yaml:"maxSpans"`
|
||||
MaxErrorDepth InterpolatedInt `yaml:"maxErrorDepth"`
|
||||
}
|
||||
|
||||
func NewDefaultSentryConfig() SentryConfig {
|
||||
return SentryConfig{
|
||||
DSN: "",
|
||||
Debug: false,
|
||||
FlushTimeout: NewInterpolatedDuration(2 * time.Second),
|
||||
AttachStacktrace: true,
|
||||
SampleRate: 1,
|
||||
EnableTracing: true,
|
||||
TracesSampleRate: 0.2,
|
||||
ProfilesSampleRate: 1,
|
||||
IgnoreErrors: []string{},
|
||||
SendDefaultPII: false,
|
||||
ServerName: "",
|
||||
Environment: "",
|
||||
MaxBreadcrumbs: 0,
|
||||
MaxSpans: 1000,
|
||||
MaxErrorDepth: 10,
|
||||
}
|
||||
}
|
4
internal/config/testdata/config.yml
vendored
4
internal/config/testdata/config.yml
vendored
@ -2,5 +2,5 @@ logger:
|
||||
level: 0
|
||||
format: human
|
||||
http:
|
||||
host: "0.0.0.0"
|
||||
port: 3000
|
||||
host: "${LISTEN_ADDR}"
|
||||
port: 3000
|
||||
|
49
internal/integration/context.go
Normal file
49
internal/integration/context.go
Normal file
@ -0,0 +1,49 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
ctxPublicKeySet contextKey = "public-key-set"
|
||||
ctxPrivateKey contextKey = "private-key"
|
||||
)
|
||||
|
||||
func CtxPublicKeySet(ctx context.Context) (jwk.Set, error) {
|
||||
return ctxValue[jwk.Set](ctx, ctxPublicKeySet)
|
||||
}
|
||||
|
||||
func WithPublicKeySet(ctx context.Context, set jwk.Set) context.Context {
|
||||
return context.WithValue(ctx, ctxPublicKeySet, set)
|
||||
}
|
||||
|
||||
func CtxPrivateKey(ctx context.Context) (jwk.Key, error) {
|
||||
return ctxValue[jwk.Key](ctx, ctxPrivateKey)
|
||||
}
|
||||
|
||||
func WithPrivateKey(ctx context.Context, key jwk.Key) context.Context {
|
||||
return context.WithValue(ctx, ctxPrivateKey, key)
|
||||
}
|
||||
|
||||
func ctxValue[T any](ctx context.Context, key contextKey) (T, error) {
|
||||
raw := ctx.Value(key)
|
||||
if raw == nil {
|
||||
return *new(T), errors.WithStack(ErrNotFound)
|
||||
}
|
||||
|
||||
value, ok := raw.(T)
|
||||
if !ok {
|
||||
return *new(T), errors.Errorf("unexpected value type '%T'", raw)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
57
internal/integration/integration.go
Normal file
57
internal/integration/integration.go
Normal file
@ -0,0 +1,57 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Integration interface {
|
||||
Integration()
|
||||
}
|
||||
|
||||
type OnStartup interface {
|
||||
Integration
|
||||
OnStartup(ctx context.Context) error
|
||||
}
|
||||
|
||||
type OnKeyLoad interface {
|
||||
Integration
|
||||
OnKeyLoad(ctx context.Context) (jwk.Key, error)
|
||||
}
|
||||
|
||||
func RunOnStartup(ctx context.Context, integrations []Integration) error {
|
||||
for _, it := range integrations {
|
||||
onStartup, ok := it.(OnStartup)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := onStartup.OnStartup(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunOnKeyLoad(ctx context.Context, integrations []Integration) (jwk.Key, error) {
|
||||
for _, it := range integrations {
|
||||
onKeyLoad, ok := it.(OnKeyLoad)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key, err := onKeyLoad.OnKeyLoad(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
293
internal/integration/kubernetes/integration.go
Normal file
293
internal/integration/kubernetes/integration.go
Normal file
@ -0,0 +1,293 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/integration"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
|
||||
writerTokenSubject = "bouncer-admin-kubernetes-writer"
|
||||
readerTokenSubject = "bouncer-admin-kubernetes-reader"
|
||||
)
|
||||
|
||||
type Integration struct {
|
||||
Options *Options
|
||||
}
|
||||
|
||||
// OnKeyLoad implements integration.OnKeyLoad.
|
||||
func (i *Integration) OnKeyLoad(ctx context.Context) (jwk.Key, error) {
|
||||
locker := i.Options.Locker
|
||||
timeout := i.Options.LockTimeout
|
||||
|
||||
var key jwk.Key
|
||||
err := locker.WithLock(ctx, "bouncer-kubernetes-onkeyload", timeout, func(ctx context.Context) error {
|
||||
client, err := i.getClient()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if i.Options.PrivateKeySecret != "" {
|
||||
sharedPrivateKey, err := i.getSharedPrivateKey(ctx, client, i.Options.PrivateKeySecretNamespace, i.Options.PrivateKeySecret)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if sharedPrivateKey != nil {
|
||||
key = sharedPrivateKey
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Integration implements integration.OnStartup.
|
||||
func (i *Integration) Integration() {}
|
||||
|
||||
// OnStartup implements integration.OnStartup.
|
||||
func (i *Integration) OnStartup(ctx context.Context) error {
|
||||
locker := i.Options.Locker
|
||||
timeout := i.Options.LockTimeout
|
||||
err := locker.WithLock(ctx, "bouncer-kubernetes-onstartup", timeout, func(ctx context.Context) error {
|
||||
client, err := i.getClient()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if i.Options.WriterTokenSecret != "" {
|
||||
if err := i.upsertTokenSecret(ctx, client, i.Options.WriterTokenSecretNamespace, i.Options.WriterTokenSecret, writerTokenSubject, jwt.RoleWriter); err != nil {
|
||||
return errors.Wrap(err, "could not upsert writer token secret")
|
||||
}
|
||||
}
|
||||
|
||||
if i.Options.ReaderTokenSecret != "" {
|
||||
if err := i.upsertTokenSecret(ctx, client, i.Options.ReaderTokenSecretNamespace, i.Options.ReaderTokenSecret, readerTokenSubject, jwt.RoleReader); err != nil {
|
||||
return errors.Wrap(err, "could not upsert reader token secret")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Integration) getClient() (*kubernetes.Clientset, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
const (
|
||||
annotationPublicKey = "bouncer.cadoles.com/public-key"
|
||||
)
|
||||
|
||||
func (i *Integration) upsertTokenSecret(ctx context.Context, client *kubernetes.Clientset, namespace string, name string, subject string, role jwt.Role) error {
|
||||
if namespace == "" {
|
||||
defaultNamespace, err := i.getCurrentNamespace()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx,
|
||||
logger.F("secretNamespace", namespace),
|
||||
logger.F("secretName", name),
|
||||
logger.F("tokenRole", role),
|
||||
logger.F("tokenSubject", subject),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "generating new token")
|
||||
|
||||
alreadyExists := true
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serr.IsNotFound(err) {
|
||||
alreadyExists = false
|
||||
} else {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
privateKey, err := integration.CtxPrivateKey(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
keySet, err := integration.CtxPublicKeySet(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
publicKeyThumbprint, err := getKeySetThumbprint(keySet)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !alreadyExists {
|
||||
token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret := &v1.Secret{
|
||||
Type: v1.SecretTypeOpaque,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Annotations: map[string]string{
|
||||
annotationPublicKey: publicKeyThumbprint,
|
||||
},
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"token": token,
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info(ctx, "creating token secret")
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
existingPublicKeyHash, exists := secret.Annotations[annotationPublicKey]
|
||||
if !exists || publicKeyThumbprint != existingPublicKeyHash {
|
||||
token, err := jwt.GenerateToken(ctx, privateKey, i.Options.Issuer, subject, role)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret.StringData = map[string]string{
|
||||
"token": token,
|
||||
}
|
||||
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
secret.Annotations[annotationPublicKey] = publicKeyThumbprint
|
||||
|
||||
logger.Info(ctx, "updating token secret")
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
logger.Info(ctx, "key did not changed, doing nothing")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Integration) getSharedPrivateKey(ctx context.Context, client *kubernetes.Clientset, namespace string, name string) (jwk.Key, error) {
|
||||
if namespace == "" {
|
||||
defaultNamespace, err := i.getCurrentNamespace()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
namespace = defaultNamespace
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx,
|
||||
logger.F("secretNamespace", namespace),
|
||||
logger.F("secretName", name),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "searching shared private key from secret")
|
||||
|
||||
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil && !k8serr.IsNotFound(err) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawPrivateKey, exists := secret.Data["key"]
|
||||
|
||||
if exists && len(rawPrivateKey) != 0 {
|
||||
key, err := jwk.ParseKey(rawPrivateKey)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
localKey, err := integration.CtxPrivateKey(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawLocalKey, err := json.Marshal(localKey)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
secret = &v1.Secret{
|
||||
Type: v1.SecretTypeOpaque,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"key": rawLocalKey,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{}); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return localKey, nil
|
||||
}
|
||||
|
||||
func (i *Integration) getCurrentNamespace() (string, error) {
|
||||
namespace, err := os.ReadFile(namespaceFile)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not retrieve current namespace")
|
||||
}
|
||||
|
||||
return string(namespace), nil
|
||||
}
|
||||
|
||||
func NewIntegration(funcs ...OptionFunc) *Integration {
|
||||
opts := NewOptions(funcs...)
|
||||
|
||||
return &Integration{
|
||||
Options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ integration.OnStartup = &Integration{}
|
||||
_ integration.OnKeyLoad = &Integration{}
|
||||
)
|
41
internal/integration/kubernetes/keyset.go
Normal file
41
internal/integration/kubernetes/keyset.go
Normal file
@ -0,0 +1,41 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func getKeySetThumbprint(set jwk.Set) (string, error) {
|
||||
data := make([][]byte, 0, set.Len())
|
||||
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
key, exists := set.Key(i)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
thumbprint, err := key.Thumbprint(crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
data = append(data, thumbprint)
|
||||
}
|
||||
|
||||
slices.SortFunc(data, bytes.Compare)
|
||||
|
||||
hash := sha256.New()
|
||||
for _, d := range data {
|
||||
if _, err := hash.Write(d); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
}
|
95
internal/integration/kubernetes/options.go
Normal file
95
internal/integration/kubernetes/options.go
Normal file
@ -0,0 +1,95 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock/memory"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
WriterTokenSecret string
|
||||
WriterTokenSecretNamespace string
|
||||
ReaderTokenSecret string
|
||||
ReaderTokenSecretNamespace string
|
||||
PrivateKeySecret string
|
||||
PrivateKeySecretNamespace string
|
||||
Issuer string
|
||||
Locker lock.Locker
|
||||
LockTimeout time.Duration
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
WriterTokenSecret: "",
|
||||
WriterTokenSecretNamespace: "",
|
||||
ReaderTokenSecret: "",
|
||||
ReaderTokenSecretNamespace: "",
|
||||
PrivateKeySecret: "",
|
||||
PrivateKeySecretNamespace: "",
|
||||
Issuer: "",
|
||||
Locker: memory.NewLocker(),
|
||||
LockTimeout: 30 * time.Second,
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithWriterTokenSecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.WriterTokenSecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithWriterTokenSecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.WriterTokenSecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithReaderTokenSecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.ReaderTokenSecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithReaderTokenSecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.ReaderTokenSecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithPrivateKeySecret(secretName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.PrivateKeySecret = secretName
|
||||
}
|
||||
}
|
||||
|
||||
func WithPrivateKeySecretNamespace(namespace string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.PrivateKeySecretNamespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithIssuer(issuer string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Issuer = issuer
|
||||
}
|
||||
}
|
||||
|
||||
func WithLocker(locker lock.Locker) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Locker = locker
|
||||
}
|
||||
}
|
||||
|
||||
func WithLockTimeout(timeout time.Duration) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.LockTimeout = timeout
|
||||
}
|
||||
}
|
@ -4,13 +4,13 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil/base58"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/oklog/ulid/v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -24,8 +24,9 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
FromRaw = jwk.FromRaw
|
||||
NewSet = jwk.NewSet
|
||||
FromRaw = jwk.FromRaw
|
||||
NewSet = jwk.NewSet
|
||||
ParseKey = jwk.ParseKey
|
||||
)
|
||||
|
||||
const AlgorithmKey = jwk.AlgorithmKey
|
||||
@ -56,7 +57,7 @@ func PublicKeySet(keys ...jwk.Key) (jwk.Set, error) {
|
||||
}
|
||||
|
||||
func LoadOrGenerate(path string, size int) (jwk.Key, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -72,7 +73,7 @@ func LoadOrGenerate(path string, size int) (jwk.Key, error) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, data, 0o640); err != nil {
|
||||
if err := os.WriteFile(path, data, 0o640); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
@ -96,6 +97,12 @@ func Generate(size int) (jwk.Key, error) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
keyID := ulid.Make().String()
|
||||
|
||||
if err := key.Set(jwk.KeyIDKey, keyID); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
|
10
internal/lock/locker.go
Normal file
10
internal/lock/locker.go
Normal file
@ -0,0 +1,10 @@
|
||||
package lock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Locker interface {
|
||||
WithLock(ctx context.Context, key string, timeout time.Duration, fn func(ctx context.Context) error) error
|
||||
}
|
45
internal/lock/memory/locker.go
Normal file
45
internal/lock/memory/locker.go
Normal file
@ -0,0 +1,45 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTimeout = errors.New("timeout")
|
||||
)
|
||||
|
||||
type Locker struct {
|
||||
lock chan struct{}
|
||||
}
|
||||
|
||||
// WithLock implements lock.Locker.
|
||||
func (l *Locker) WithLock(ctx context.Context, key string, timeout time.Duration, fn func(ctx context.Context) error) error {
|
||||
select {
|
||||
case l.lock <- struct{}{}:
|
||||
defer func() {
|
||||
<-l.lock
|
||||
}()
|
||||
if err := fn(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return errors.WithStack(ctx.Err())
|
||||
|
||||
case <-time.After(timeout):
|
||||
return errors.WithStack(ErrTimeout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLocker() *Locker {
|
||||
return &Locker{
|
||||
lock: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
var _ lock.Locker = &Locker{}
|
67
internal/lock/redis/locker.go
Normal file
67
internal/lock/redis/locker.go
Normal file
@ -0,0 +1,67 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/lock"
|
||||
"github.com/bsm/redislock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Locker struct {
|
||||
client redis.UniversalClient
|
||||
maxRetries int
|
||||
}
|
||||
|
||||
// WithLock implements lock.Locker.
|
||||
func (l *Locker) WithLock(ctx context.Context, key string, timeout time.Duration, fn func(ctx context.Context) error) error {
|
||||
locker := redislock.New(l.client)
|
||||
|
||||
backoff := redislock.ExponentialBackoff(time.Second, timeout*2)
|
||||
|
||||
ctx = logger.With(ctx, logger.F("lockTimeout", timeout), logger.F("lockKey", key))
|
||||
|
||||
logger.Debug(ctx, "acquiring lock")
|
||||
|
||||
err := retryWithBackoff(ctx, l.maxRetries, func(ctx context.Context) error {
|
||||
lock, err := locker.Obtain(ctx, key, timeout, &redislock.Options{
|
||||
RetryStrategy: backoff,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "lock obtained")
|
||||
|
||||
defer func() {
|
||||
if err := lock.Release(ctx); err != nil {
|
||||
logger.Error(ctx, "could not release lock", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "lock released")
|
||||
}()
|
||||
|
||||
if err := fn(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLocker(client redis.UniversalClient, maxRetries int) *Locker {
|
||||
return &Locker{
|
||||
client: client,
|
||||
maxRetries: maxRetries,
|
||||
}
|
||||
}
|
||||
|
||||
var _ lock.Locker = &Locker{}
|
42
internal/lock/redis/retry.go
Normal file
42
internal/lock/redis/retry.go
Normal file
@ -0,0 +1,42 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
baseWatchBackoffDelay = time.Millisecond * 500
|
||||
maxDelay = time.Minute * 10
|
||||
)
|
||||
|
||||
func retryWithBackoff(ctx context.Context, attempts int, fn func(ctx context.Context) error) error {
|
||||
backoffDelay := baseWatchBackoffDelay
|
||||
count := 0
|
||||
|
||||
for {
|
||||
err := fn(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
|
||||
count++
|
||||
if count >= attempts {
|
||||
return errors.Wrapf(err, "execution failed after %d attempts", attempts)
|
||||
}
|
||||
|
||||
logger.Error(ctx, "error while executing func, retrying with backoff", logger.E(err), logger.F("backoffDelay", backoffDelay), logger.F("remainingAttempts", attempts-count))
|
||||
|
||||
time.Sleep(backoffDelay)
|
||||
|
||||
backoffDelay *= 2
|
||||
if backoffDelay > maxDelay {
|
||||
backoffDelay = maxDelay
|
||||
}
|
||||
}
|
||||
}
|
43
internal/logger/writer.go
Normal file
43
internal/logger/writer.go
Normal file
@ -0,0 +1,43 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
ctx context.Context
|
||||
level logger.Level
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
w.log(string(p))
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *Writer) log(message string) {
|
||||
switch w.level {
|
||||
case logger.LevelDebug:
|
||||
logger.Debug(w.ctx, message)
|
||||
case logger.LevelInfo:
|
||||
logger.Info(w.ctx, message)
|
||||
case logger.LevelWarn:
|
||||
logger.Warn(w.ctx, message)
|
||||
case logger.LevelError:
|
||||
logger.Error(w.ctx, message)
|
||||
case logger.LevelCritical:
|
||||
logger.Critical(w.ctx, message)
|
||||
default:
|
||||
logger.Debug(w.ctx, message)
|
||||
}
|
||||
}
|
||||
|
||||
func NewWriter(ctx context.Context, level logger.Level) *Writer {
|
||||
return &Writer{ctx, level}
|
||||
}
|
||||
|
||||
var _ io.Writer = &Writer{}
|
@ -2,6 +2,7 @@ package director
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
@ -10,8 +11,9 @@ import (
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
contextKeyProxy contextKey = "proxy"
|
||||
contextKeyLayers contextKey = "layers"
|
||||
contextKeyProxy contextKey = "proxy"
|
||||
contextKeyLayers contextKey = "layers"
|
||||
contextKeyOriginalURL contextKey = "originalURL"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -19,6 +21,19 @@ var (
|
||||
errUnexpectedContextValue = errors.New("unexpected context value")
|
||||
)
|
||||
|
||||
func withOriginalURL(ctx context.Context, url *url.URL) context.Context {
|
||||
return context.WithValue(ctx, contextKeyOriginalURL, url)
|
||||
}
|
||||
|
||||
func OriginalURL(ctx context.Context) (*url.URL, error) {
|
||||
url, err := ctxValue[*url.URL](ctx, contextKeyOriginalURL)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func withProxy(ctx context.Context, proxy *store.Proxy) context.Context {
|
||||
return context.WithValue(ctx, contextKeyProxy, proxy)
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ package director
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy"
|
||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
@ -27,15 +27,29 @@ func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
|
||||
return r, errors.WithStack(err)
|
||||
}
|
||||
|
||||
url := getRequestURL(r)
|
||||
|
||||
ctx = withOriginalURL(ctx, url)
|
||||
ctx = logger.With(ctx, logger.F("url", url.String()))
|
||||
|
||||
var match *store.Proxy
|
||||
|
||||
MAIN:
|
||||
for _, p := range proxies {
|
||||
for _, from := range p.From {
|
||||
if matches := wildcard.Match(r.Host, from); !matches {
|
||||
logger.Debug(
|
||||
ctx, "matching request with proxy's from",
|
||||
logger.F("from", from),
|
||||
)
|
||||
if matches := wildcard.Match(url.String(), from); !matches {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(
|
||||
ctx, "proxy's from matched",
|
||||
logger.F("from", from),
|
||||
)
|
||||
|
||||
match = p
|
||||
break MAIN
|
||||
}
|
||||
@ -59,6 +73,8 @@ MAIN:
|
||||
logger.F("remoteAddr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(match.Name)}).Add(1)
|
||||
|
||||
ctx = withProxy(ctx, match)
|
||||
|
||||
layers, err := d.getLayers(ctx, match.Name)
|
||||
|
26
internal/proxy/director/layer/authn/authenticator.go
Normal file
26
internal/proxy/director/layer/authn/authenticator.go
Normal file
@ -0,0 +1,26 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrSkipRequest = errors.New("skip request")
|
||||
)
|
||||
|
||||
type Authenticator interface {
|
||||
Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*User, error)
|
||||
}
|
||||
|
||||
type PreAuthentication interface {
|
||||
PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer) error
|
||||
}
|
||||
|
||||
type PostAuthentication interface {
|
||||
PostAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer, user *User) error
|
||||
}
|
94
internal/proxy/director/layer/authn/header.go
Normal file
94
internal/proxy/director/layer/authn/header.go
Normal file
@ -0,0 +1,94 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (l *Layer) getRuleOptions(r *http.Request) []expr.Option {
|
||||
options := make([]expr.Option, 0)
|
||||
|
||||
setHeader := expr.Function(
|
||||
"set_header",
|
||||
func(params ...any) (any, error) {
|
||||
name := params[0].(string)
|
||||
rawValue := params[1]
|
||||
|
||||
var value string
|
||||
switch v := rawValue.(type) {
|
||||
case []string:
|
||||
value = strings.Join(v, ",")
|
||||
case time.Time:
|
||||
value = strconv.FormatInt(v.UTC().Unix(), 10)
|
||||
case time.Duration:
|
||||
value = strconv.FormatInt(int64(v.Seconds()), 10)
|
||||
default:
|
||||
value = fmt.Sprintf("%v", rawValue)
|
||||
}
|
||||
|
||||
r.Header.Set(name, value)
|
||||
|
||||
return true, nil
|
||||
},
|
||||
new(func(string, string) bool),
|
||||
)
|
||||
|
||||
options = append(options, setHeader)
|
||||
|
||||
delHeaders := expr.Function(
|
||||
"del_headers",
|
||||
func(params ...any) (any, error) {
|
||||
pattern := params[0].(string)
|
||||
deleted := false
|
||||
|
||||
for key := range r.Header {
|
||||
if !wildcard.Match(key, pattern) {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Header.Del(key)
|
||||
deleted = true
|
||||
}
|
||||
|
||||
return deleted, nil
|
||||
},
|
||||
new(func(string) bool),
|
||||
)
|
||||
|
||||
options = append(options, delHeaders)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (l *Layer) injectHeaders(r *http.Request, options *LayerOptions, user *User) error {
|
||||
rules := options.Headers.Rules
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
env := map[string]any{
|
||||
"user": user,
|
||||
}
|
||||
|
||||
rulesOptions := l.getRuleOptions(r)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if _, err := expr.Run(program, env); err != nil {
|
||||
return errors.Wrapf(err, "could not execute header rule #%d", i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
38
internal/proxy/director/layer/authn/layer-options.json
Normal file
38
internal/proxy/director/layer/authn/layer-options.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/authn-options",
|
||||
"title": "Options de configuration commune des layers 'authn-*'",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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)) })"
|
||||
],
|
||||
"item": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
103
internal/proxy/director/layer/authn/layer.go
Normal file
103
internal/proxy/director/layer/authn/layer.go
Normal file
@ -0,0 +1,103 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Layer struct {
|
||||
layerType store.LayerType
|
||||
auth Authenticator
|
||||
}
|
||||
|
||||
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return func(next 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
|
||||
}
|
||||
|
||||
if preAuth, ok := l.auth.(PreAuthentication); ok {
|
||||
if err := preAuth.PreAuthentication(w, r, layer); err != nil {
|
||||
if errors.Is(err, ErrSkipRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not execute pre-auth hook", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
|
||||
if !matches {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := l.auth.Authenticate(w, r, layer)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrSkipRequest) {
|
||||
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)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if postAuth, ok := l.auth.(PostAuthentication); ok {
|
||||
if err := postAuth.PostAuthentication(w, r, layer, user); err != nil {
|
||||
if errors.Is(err, ErrSkipRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer
|
||||
func (l *Layer) LayerType() store.LayerType {
|
||||
return l.layerType
|
||||
}
|
||||
|
||||
func NewLayer(layerType store.LayerType, auth Authenticator) *Layer {
|
||||
return &Layer{
|
||||
layerType: layerType,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
var _ director.MiddlewareLayer = &Layer{}
|
92
internal/proxy/director/layer/authn/layer_options.go
Normal file
92
internal/proxy/director/layer/authn/layer_options.go
Normal file
@ -0,0 +1,92 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LayerOptions struct {
|
||||
MatchURLs []string `mapstructure:"matchURLs"`
|
||||
Headers HeadersOptions `mapstructure:"headers"`
|
||||
}
|
||||
|
||||
type HeadersOptions struct {
|
||||
Rules []string `mapstructure:"rules"`
|
||||
}
|
||||
|
||||
func DefaultLayerOptions() LayerOptions {
|
||||
return LayerOptions{
|
||||
MatchURLs: []string{"*"},
|
||||
Headers: HeadersOptions{
|
||||
Rules: []string{
|
||||
"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)
|
||||
)
|
||||
})
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||
layerOptions := DefaultLayerOptions()
|
||||
|
||||
if err := FromStoreOptions(storeOptions, &layerOptions); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &layerOptions, nil
|
||||
}
|
||||
|
||||
func FromStoreOptions(storeOptions store.LayerOptions, dest any) error {
|
||||
config := mapstructure.DecoderConfig{
|
||||
Result: dest,
|
||||
ZeroFields: true,
|
||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||
toDurationHookFunc(),
|
||||
),
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(&config)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := decoder.Decode(storeOptions); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toDurationHookFunc() mapstructure.DecodeHookFunc {
|
||||
return func(
|
||||
f reflect.Type,
|
||||
t reflect.Type,
|
||||
data interface{}) (interface{}, error) {
|
||||
if t != reflect.TypeOf(*new(time.Duration)) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
switch f.Kind() {
|
||||
case reflect.String:
|
||||
return time.ParseDuration(data.(string))
|
||||
case reflect.Int64:
|
||||
return time.Duration(data.(int64) * int64(time.Second)), nil
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
// Convert it by parsing
|
||||
}
|
||||
}
|
278
internal/proxy/director/layer/authn/oidc/authenticator.go
Normal file
278
internal/proxy/director/layer/authn/oidc/authenticator.go
Normal file
@ -0,0 +1,278 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
store sessions.Store
|
||||
}
|
||||
|
||||
func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer) error {
|
||||
ctx := r.Context()
|
||||
|
||||
originalURL, err := director.OriginalURL(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
options, err := fromStoreOptions(layer.Options)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name))
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
redirectURL := a.getRedirectURL(layer.Proxy, layer.Name, originalURL, options)
|
||||
logoutURL := a.getLogoutURL(layer.Proxy, layer.Name, originalURL, options)
|
||||
|
||||
client, err := a.getClient(options, redirectURL.String())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
switch r.URL.Path {
|
||||
case redirectURL.Path:
|
||||
if err := client.HandleCallback(w, r, sess); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
metricLoginSuccessesTotal.With(prometheus.Labels{
|
||||
metricLabelLayer: string(layer.Name),
|
||||
metricLabelProxy: string(layer.Proxy),
|
||||
}).Add(1)
|
||||
|
||||
case logoutURL.Path:
|
||||
postLogoutRedirectURL := options.OIDC.PostLogoutRedirectURL
|
||||
if options.OIDC.PostLogoutRedirectURL == "" {
|
||||
postLogoutRedirectURL = originalURL.Scheme + "://" + originalURL.Host
|
||||
}
|
||||
|
||||
if err := client.HandleLogout(w, r, sess, postLogoutRedirectURL); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
metricLogoutsTotal.With(prometheus.Labels{
|
||||
metricLabelLayer: string(layer.Name),
|
||||
metricLabelProxy: string(layer.Proxy),
|
||||
}).Add(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate implements authn.Authenticator.
|
||||
func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
originalURL, err := director.OriginalURL(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
options, err := fromStoreOptions(layer.Options)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
sess.Options.Domain = options.Cookie.Domain
|
||||
sess.Options.HttpOnly = options.Cookie.HTTPOnly
|
||||
sess.Options.MaxAge = int(options.Cookie.MaxAge.Seconds())
|
||||
sess.Options.Path = options.Cookie.Path
|
||||
|
||||
switch options.Cookie.SameSite {
|
||||
case "lax":
|
||||
sess.Options.SameSite = http.SameSiteLaxMode
|
||||
case "strict":
|
||||
sess.Options.SameSite = http.SameSiteStrictMode
|
||||
case "none":
|
||||
sess.Options.SameSite = http.SameSiteNoneMode
|
||||
default:
|
||||
sess.Options.SameSite = http.SameSiteDefaultMode
|
||||
}
|
||||
|
||||
redirectURL := a.getRedirectURL(layer.Proxy, layer.Name, originalURL, options)
|
||||
|
||||
client, err := a.getClient(options, redirectURL.String())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
idToken, err := client.Authenticate(w, r, sess)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrLoginRequired) {
|
||||
metricLoginRequestsTotal.With(prometheus.Labels{
|
||||
metricLabelLayer: string(layer.Name),
|
||||
metricLabelProxy: string(layer.Proxy),
|
||||
}).Add(1)
|
||||
|
||||
return nil, errors.WithStack(authn.ErrSkipRequest)
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user, err := a.toUser(idToken, layer.Proxy, layer.Name, originalURL, options, sess)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Expiration int64 `json:"exp"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
AuthTime int64 `json:"auth_time"`
|
||||
Nonce string `json:"nonce"`
|
||||
ACR string `json:"acr"`
|
||||
AMR string `json:"amr"`
|
||||
AZP string `json:"amp"`
|
||||
Others map[string]any `json:"-"`
|
||||
}
|
||||
|
||||
func (c claims) AsAttrs() map[string]any {
|
||||
attrs := make(map[string]any)
|
||||
|
||||
for key, val := range c.Others {
|
||||
if val != nil {
|
||||
attrs["claim_"+key] = val
|
||||
}
|
||||
}
|
||||
|
||||
attrs["claim_iss"] = c.Issuer
|
||||
attrs["claim_sub"] = c.Subject
|
||||
attrs["claim_exp"] = c.Expiration
|
||||
attrs["claim_iat"] = c.IssuedAt
|
||||
|
||||
if c.AuthTime != 0 {
|
||||
attrs["claim_auth_time"] = c.AuthTime
|
||||
}
|
||||
|
||||
if c.Nonce != "" {
|
||||
attrs["claim_nonce"] = c.Nonce
|
||||
}
|
||||
|
||||
if c.ACR != "" {
|
||||
attrs["claim_arc"] = c.ACR
|
||||
}
|
||||
|
||||
if c.AMR != "" {
|
||||
attrs["claim_amr"] = c.AMR
|
||||
}
|
||||
|
||||
if c.AZP != "" {
|
||||
attrs["claim_azp"] = c.AZP
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
func (a *Authenticator) toUser(idToken *oidc.IDToken, proxyName store.ProxyName, layerName store.LayerName, originalURL *url.URL, options *LayerOptions, sess *sessions.Session) (*authn.User, error) {
|
||||
var claims claims
|
||||
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := idToken.Claims(&claims.Others); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
attrs := claims.AsAttrs()
|
||||
|
||||
logoutURL := a.getLogoutURL(proxyName, layerName, originalURL, options)
|
||||
attrs["logout_url"] = logoutURL.String()
|
||||
|
||||
if accessToken, exists := sess.Values[sessionKeyAccessToken]; exists && accessToken != nil {
|
||||
attrs["access_token"] = accessToken
|
||||
}
|
||||
|
||||
if refreshToken, exists := sess.Values[sessionKeyRefreshToken]; exists && refreshToken != nil {
|
||||
attrs["refresh_token"] = refreshToken
|
||||
}
|
||||
|
||||
if tokenExpiry, exists := sess.Values[sessionKeyTokenExpiry]; exists && tokenExpiry != nil {
|
||||
attrs["token_expiry"] = tokenExpiry
|
||||
}
|
||||
|
||||
user := authn.NewUser(idToken.Subject, attrs)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getRedirectURL(proxyName store.ProxyName, layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Path: fmt.Sprintf(options.OIDC.LoginCallbackPath, fmt.Sprintf("%s/%s", proxyName, layerName)),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Authenticator) getLogoutURL(proxyName store.ProxyName, layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL {
|
||||
return &url.URL{
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Path: fmt.Sprintf(options.OIDC.LogoutPath, fmt.Sprintf("%s/%s", proxyName, layerName)),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*Client, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
if options.OIDC.SkipIssuerVerification {
|
||||
ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL)
|
||||
}
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, options.OIDC.IssuerURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create oidc provider")
|
||||
}
|
||||
|
||||
client := NewClient(
|
||||
WithCredentials(options.OIDC.ClientID, options.OIDC.ClientSecret),
|
||||
WithProvider(provider),
|
||||
WithRedirectURL(redirectURL),
|
||||
WithScopes(options.OIDC.Scopes...),
|
||||
WithAuthParams(options.OIDC.AuthParams),
|
||||
)
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (a *Authenticator) getCookieName(cookieName string, layerName store.LayerName) string {
|
||||
return fmt.Sprintf("%s_%s", cookieName, layerName)
|
||||
}
|
||||
|
||||
var (
|
||||
_ authn.PreAuthentication = &Authenticator{}
|
||||
_ authn.Authenticator = &Authenticator{}
|
||||
)
|
291
internal/proxy/director/layer/authn/oidc/client.go
Normal file
291
internal/proxy/director/layer/authn/oidc/client.go
Normal file
@ -0,0 +1,291 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionKeyAccessToken = "access-token"
|
||||
sessionKeyRefreshToken = "refresh-token"
|
||||
sessionKeyTokenExpiry = "token-expiry"
|
||||
sessionKeyIDToken = "id-token"
|
||||
sessionKeyPostLoginRedirectURL = "post-login-redirect-url"
|
||||
sessionKeyLoginState = "login-state"
|
||||
sessionKeyLoginNonce = "login-nonce"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLoginRequired = errors.New("login required")
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
oauth2 *oauth2.Config
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
authParams map[string]string
|
||||
}
|
||||
|
||||
func (c *Client) Verifier() *oidc.IDTokenVerifier {
|
||||
return c.verifier
|
||||
}
|
||||
|
||||
func (c *Client) Provider() *oidc.Provider {
|
||||
return c.provider
|
||||
}
|
||||
|
||||
func (c *Client) Authenticate(w http.ResponseWriter, r *http.Request, sess *sessions.Session) (*oidc.IDToken, error) {
|
||||
idToken, err := c.getIDToken(r, sess)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not retrieve idtoken", logger.E(errors.WithStack(err)))
|
||||
|
||||
c.login(w, r, sess)
|
||||
|
||||
return nil, errors.WithStack(ErrLoginRequired)
|
||||
}
|
||||
|
||||
return idToken, nil
|
||||
}
|
||||
|
||||
func (c *Client) login(w http.ResponseWriter, r *http.Request, sess *sessions.Session) {
|
||||
ctx := r.Context()
|
||||
|
||||
state := uniuri.New()
|
||||
nonce := uniuri.New()
|
||||
|
||||
originalURL, err := director.OriginalURL(ctx)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve original url", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sess.Values[sessionKeyLoginState] = state
|
||||
sess.Values[sessionKeyLoginNonce] = nonce
|
||||
sess.Values[sessionKeyPostLoginRedirectURL] = originalURL.String()
|
||||
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
authCodeOptions := []oauth2.AuthCodeOption{}
|
||||
authCodeOptions = append(authCodeOptions, oidc.Nonce(nonce))
|
||||
|
||||
for key, val := range c.authParams {
|
||||
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val))
|
||||
}
|
||||
|
||||
authCodeURL := c.oauth2.AuthCodeURL(
|
||||
state,
|
||||
authCodeOptions...,
|
||||
)
|
||||
|
||||
http.Redirect(w, r, authCodeURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (c *Client) HandleCallback(w http.ResponseWriter, r *http.Request, sess *sessions.Session) error {
|
||||
token, _, rawIDToken, err := c.validate(r, sess)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not validate oidc token")
|
||||
}
|
||||
|
||||
sess.Values[sessionKeyIDToken] = rawIDToken
|
||||
sess.Values[sessionKeyAccessToken] = token.AccessToken
|
||||
sess.Values[sessionKeyRefreshToken] = token.RefreshToken
|
||||
sess.Values[sessionKeyTokenExpiry] = token.Expiry.UTC().Unix()
|
||||
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawPostLoginRedirectURL, exists := sess.Values[sessionKeyPostLoginRedirectURL]
|
||||
if !exists {
|
||||
return errors.Wrap(err, "could not find post login redirect url")
|
||||
}
|
||||
|
||||
postLoginRedirectURL, ok := rawPostLoginRedirectURL.(string)
|
||||
if !ok {
|
||||
return errors.Wrapf(err, "unexpected value '%v' for post login redirect url", rawPostLoginRedirectURL)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, postLoginRedirectURL, http.StatusTemporaryRedirect)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) HandleLogout(w http.ResponseWriter, r *http.Request, sess *sessions.Session, postLogoutRedirectURL string) error {
|
||||
state := uniuri.New()
|
||||
sess.Values[sessionKeyLoginState] = state
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
rawIDToken, err := c.getRawIDToken(sess)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve raw id token", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
|
||||
sess.Values[sessionKeyIDToken] = nil
|
||||
sess.Values[sessionKeyAccessToken] = nil
|
||||
sess.Values[sessionKeyRefreshToken] = nil
|
||||
sess.Values[sessionKeyTokenExpiry] = nil
|
||||
sess.Options.MaxAge = -1
|
||||
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
return errors.Wrap(err, "could not save session")
|
||||
}
|
||||
|
||||
if rawIDToken == "" {
|
||||
http.Redirect(w, r, postLogoutRedirectURL, http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionEndURL, err := c.sessionEndURL(rawIDToken, state, postLogoutRedirectURL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve session end url")
|
||||
}
|
||||
|
||||
if sessionEndURL != "" {
|
||||
http.Redirect(w, r, sessionEndURL, http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(w, r, postLogoutRedirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) sessionEndURL(idTokenHint, state, postLogoutRedirectURL string) (string, error) {
|
||||
sessionEndEndpoint := &struct {
|
||||
URL string `json:"end_session_endpoint"`
|
||||
}{}
|
||||
|
||||
if err := c.provider.Claims(&sessionEndEndpoint); err != nil {
|
||||
return "", errors.Wrap(err, "could not unmarshal claims")
|
||||
}
|
||||
|
||||
if sessionEndEndpoint.URL == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(sessionEndEndpoint.URL)
|
||||
|
||||
v := url.Values{}
|
||||
|
||||
if idTokenHint != "" {
|
||||
v.Set("id_token_hint", idTokenHint)
|
||||
}
|
||||
|
||||
if postLogoutRedirectURL != "" {
|
||||
v.Set("post_logout_redirect_uri", postLogoutRedirectURL)
|
||||
}
|
||||
|
||||
if state != "" {
|
||||
v.Set("state", state)
|
||||
}
|
||||
|
||||
if strings.Contains(sessionEndEndpoint.URL, "?") {
|
||||
buf.WriteByte('&')
|
||||
} else {
|
||||
buf.WriteByte('?')
|
||||
}
|
||||
|
||||
buf.WriteString(v.Encode())
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (c *Client) validate(r *http.Request, sess *sessions.Session) (*oauth2.Token, *oidc.IDToken, string, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
rawStoredState := sess.Values[sessionKeyLoginState]
|
||||
receivedState := r.URL.Query().Get("state")
|
||||
|
||||
storedState, ok := rawStoredState.(string)
|
||||
if !ok {
|
||||
return nil, nil, "", errors.New("could not find state in session")
|
||||
}
|
||||
|
||||
if receivedState != storedState {
|
||||
return nil, nil, "", errors.New("state mismatch")
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
|
||||
token, err := c.oauth2.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, nil, "", errors.Wrap(err, "could not exchange token")
|
||||
}
|
||||
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, nil, "", errors.New("could not find id token")
|
||||
}
|
||||
|
||||
idToken, err := c.verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, nil, "", errors.Wrap(err, "could not verify id token")
|
||||
}
|
||||
|
||||
return token, idToken, rawIDToken, nil
|
||||
}
|
||||
|
||||
func (c *Client) getRawIDToken(sess *sessions.Session) (string, error) {
|
||||
rawIDToken, ok := sess.Values[sessionKeyIDToken].(string)
|
||||
if !ok || rawIDToken == "" {
|
||||
return "", errors.New("invalid id token")
|
||||
}
|
||||
|
||||
return rawIDToken, nil
|
||||
}
|
||||
|
||||
func (c *Client) getIDToken(r *http.Request, sess *sessions.Session) (*oidc.IDToken, error) {
|
||||
rawIDToken, err := c.getRawIDToken(sess)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve raw idtoken")
|
||||
}
|
||||
|
||||
idToken, err := c.verifier.Verify(r.Context(), rawIDToken)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not verify id token")
|
||||
}
|
||||
|
||||
return idToken, nil
|
||||
}
|
||||
|
||||
func NewClient(funcs ...ClientOptionFunc) *Client {
|
||||
opts := NewClientOptions(funcs...)
|
||||
|
||||
oauth2 := &oauth2.Config{
|
||||
ClientID: opts.ClientID,
|
||||
ClientSecret: opts.ClientSecret,
|
||||
Endpoint: opts.Provider.Endpoint(),
|
||||
RedirectURL: opts.RedirectURL,
|
||||
Scopes: opts.Scopes,
|
||||
}
|
||||
|
||||
verifier := opts.Provider.Verifier(&oidc.Config{
|
||||
ClientID: opts.ClientID,
|
||||
SkipIssuerCheck: opts.SkipIssuerCheck,
|
||||
})
|
||||
|
||||
return &Client{
|
||||
oauth2: oauth2,
|
||||
provider: opts.Provider,
|
||||
verifier: verifier,
|
||||
authParams: opts.AuthParams,
|
||||
}
|
||||
}
|
76
internal/proxy/director/layer/authn/oidc/client_options.go
Normal file
76
internal/proxy/director/layer/authn/oidc/client_options.go
Normal file
@ -0,0 +1,76 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
)
|
||||
|
||||
type ClientOptions struct {
|
||||
Provider *oidc.Provider
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
RedirectURL string
|
||||
Scopes []string
|
||||
AuthParams map[string]string
|
||||
SkipIssuerCheck bool
|
||||
}
|
||||
|
||||
type ClientOptionFunc func(*ClientOptions)
|
||||
|
||||
func WithRedirectURL(url string) ClientOptionFunc {
|
||||
return func(opt *ClientOptions) {
|
||||
opt.RedirectURL = url
|
||||
}
|
||||
}
|
||||
|
||||
func WithCredentials(clientID, clientSecret string) ClientOptionFunc {
|
||||
return func(opt *ClientOptions) {
|
||||
opt.ClientID = clientID
|
||||
opt.ClientSecret = clientSecret
|
||||
}
|
||||
}
|
||||
|
||||
func WithScopes(scopes ...string) ClientOptionFunc {
|
||||
return func(opt *ClientOptions) {
|
||||
opt.Scopes = scopes
|
||||
}
|
||||
}
|
||||
|
||||
func WithAuthParams(params map[string]string) ClientOptionFunc {
|
||||
return func(opt *ClientOptions) {
|
||||
opt.AuthParams = params
|
||||
}
|
||||
}
|
||||
|
||||
func WithSkipIssuerCheck(skip bool) ClientOptionFunc {
|
||||
return func(opt *ClientOptions) {
|
||||
opt.SkipIssuerCheck = skip
|
||||
}
|
||||
}
|
||||
|
||||
func NewProvider(ctx context.Context, issuer string, skipIssuerVerification bool) (*oidc.Provider, error) {
|
||||
if skipIssuerVerification {
|
||||
ctx = oidc.InsecureIssuerURLContext(ctx, issuer)
|
||||
}
|
||||
|
||||
return oidc.NewProvider(ctx, issuer)
|
||||
}
|
||||
|
||||
func WithProvider(provider *oidc.Provider) ClientOptionFunc {
|
||||
return func(opt *ClientOptions) {
|
||||
opt.Provider = provider
|
||||
}
|
||||
}
|
||||
|
||||
func NewClientOptions(funcs ...ClientOptionFunc) *ClientOptions {
|
||||
opt := &ClientOptions{
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile"},
|
||||
}
|
||||
|
||||
for _, f := range funcs {
|
||||
f(opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
127
internal/proxy/director/layer/authn/oidc/layer-options.json
Normal file
127
internal/proxy/director/layer/authn/oidc/layer-options.json
Normal file
@ -0,0 +1,127 @@
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/authn-oidc-layer-options",
|
||||
"title": "Options de configuration du layer 'authn-oidc'",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"oidc": {
|
||||
"title": "Configuration du client OpenID Connect",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clientId": {
|
||||
"title": "Identifiant du client OpenID Connect",
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"title": "Secret du client OpenID Connect",
|
||||
"type": "string"
|
||||
},
|
||||
"issuerURL": {
|
||||
"title": "URL de base du fournisseur OpenID Connect (racine du .well-known/openid-configuration)",
|
||||
"type": "string"
|
||||
},
|
||||
"postLogoutRedirectURL": {
|
||||
"title": "URL de redirection après déconnexion",
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"title": "Scopes associés au client OpenID Connect",
|
||||
"default": [
|
||||
"openid"
|
||||
],
|
||||
"type": "array",
|
||||
"item": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"authParams": {
|
||||
"title": "Paramètres d'URL supplémentaires à ajouter à la requête d'authentification OpenID Connect",
|
||||
"default": {},
|
||||
"description": "L'ensemble des clés valeurs renseignées seront transformées en variables d'URL lors de la requête d'authentification initiale. Permet par exemple d'ajouter les 'acr_values' requises par certains fournisseurs d'identité OpenID Connect.",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"loginCallbackPath": {
|
||||
"title": "Chemin associé à l'URL de callback OpenID Connect",
|
||||
"default": "/.bouncer/authn/oidc/%s/callback",
|
||||
"description": "Le marqueur '%s' peut être utilisé pour injecter l'espace de nom '<proxy>/<layer>'.",
|
||||
"type": "string"
|
||||
},
|
||||
"logoutPath": {
|
||||
"title": "Chemin associé à l'URL de déconnexion",
|
||||
"default": "/.bouncer/authn/oidc/%s/logout",
|
||||
"description": "Le marqueur '%s' peut être utilisé pour injecter l'espace de nom '<proxy>/<layer>'.",
|
||||
"type": "string"
|
||||
},
|
||||
"skipIssuerVerification": {
|
||||
"title": "Activer/désactiver la vérification de concordance de l'identifiant du fournisseur d'identité",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuerURL"
|
||||
]
|
||||
},
|
||||
"cookie": {
|
||||
"title": "Configuration du cookie porteur de la session utilisateur",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Nom du cookie",
|
||||
"default": "_bouncer_authn_oidc",
|
||||
"type": "string"
|
||||
},
|
||||
"domain": {
|
||||
"title": "Domaine associé au cookie",
|
||||
"description": "Par défaut le domaine associé à la requête HTTP",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"title": "Chemin associé au cookie",
|
||||
"type": "string",
|
||||
"default": "/"
|
||||
},
|
||||
"sameSite": {
|
||||
"title": "Attribut sameSite du cookie",
|
||||
"description": "Voir https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"lax",
|
||||
"none",
|
||||
"strict",
|
||||
""
|
||||
],
|
||||
"default": ""
|
||||
},
|
||||
"httpOnly": {
|
||||
"title": "Interdire ou non l'accès au cookie en Javascript",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"secure": {
|
||||
"title": "Transmettre le cookie uniquement en HTTPS",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"maxAge": {
|
||||
"title": "Temps de vie du cookie et de la session associée.",
|
||||
"description": "Voir https://pkg.go.dev/time#ParseDuration pour le format attendu.",
|
||||
"default": "1h",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"oidc"
|
||||
]
|
||||
}
|
13
internal/proxy/director/layer/authn/oidc/layer.go
Normal file
13
internal/proxy/director/layer/authn/oidc/layer.go
Normal file
@ -0,0 +1,13 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
const LayerType store.LayerType = "authn-oidc"
|
||||
|
||||
func NewLayer(store sessions.Store) *authn.Layer {
|
||||
return authn.NewLayer(LayerType, &Authenticator{store: store})
|
||||
}
|
62
internal/proxy/director/layer/authn/oidc/layer_options.go
Normal file
62
internal/proxy/director/layer/authn/oidc/layer_options.go
Normal file
@ -0,0 +1,62 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const defaultCookieName = "_bouncer_authn_oidc"
|
||||
|
||||
type LayerOptions struct {
|
||||
authn.LayerOptions
|
||||
OIDC OIDCOptions `mapstructure:"oidc"`
|
||||
Cookie CookieOptions `mapstructure:"cookie"`
|
||||
}
|
||||
|
||||
type OIDCOptions struct {
|
||||
ClientID string `mapstructure:"clientId"`
|
||||
ClientSecret string `mapstructure:"clientSecret"`
|
||||
LoginCallbackPath string `mapstructure:"loginCallbackPath"`
|
||||
LogoutPath string `mapstructure:"logoutPath"`
|
||||
IssuerURL string `mapstructure:"issuerURL"`
|
||||
SkipIssuerVerification bool `mapstructure:"skipIssuerVerification"`
|
||||
PostLogoutRedirectURL string `mapstructure:"postLogoutRedirectURL"`
|
||||
Scopes []string `mapstructure:"scopes"`
|
||||
AuthParams map[string]string `mapstructure:"authParams"`
|
||||
}
|
||||
|
||||
type CookieOptions struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
Path string `mapstructure:"path"`
|
||||
SameSite string `mapstructure:"sameSite"`
|
||||
Secure bool `mapstructure:"secure"`
|
||||
HTTPOnly bool `mapstructure:"httpOnly"`
|
||||
MaxAge time.Duration `mapstructure:"maxAge"`
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||
layerOptions := LayerOptions{
|
||||
LayerOptions: authn.DefaultLayerOptions(),
|
||||
OIDC: OIDCOptions{
|
||||
LoginCallbackPath: "/.bouncer/authn/oidc/%s/callback",
|
||||
LogoutPath: "/.bouncer/authn/oidc/%s/logout",
|
||||
Scopes: []string{"openid"},
|
||||
},
|
||||
Cookie: CookieOptions{
|
||||
Name: defaultCookieName,
|
||||
Path: "/",
|
||||
HTTPOnly: true,
|
||||
MaxAge: time.Hour,
|
||||
},
|
||||
}
|
||||
|
||||
if err := authn.FromStoreOptions(storeOptions, &layerOptions); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &layerOptions, nil
|
||||
}
|
39
internal/proxy/director/layer/authn/oidc/metrics.go
Normal file
39
internal/proxy/director/layer/authn/oidc/metrics.go
Normal file
@ -0,0 +1,39 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const (
|
||||
metricNamespace = "bouncer_layer_authn_oidc"
|
||||
metricLabelProxy = "proxy"
|
||||
metricLabelLayer = "layer"
|
||||
)
|
||||
|
||||
var (
|
||||
metricLoginRequestsTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "login_requests_total",
|
||||
Help: "Bouncer's authn-oidc layer total login requests",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
metricLoginSuccessesTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "login_successes_total",
|
||||
Help: "Bouncer's authn-oidc layer total login successes",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
metricLogoutsTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "logout_total",
|
||||
Help: "Bouncer's authn-oidc layer total logouts",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
)
|
8
internal/proxy/director/layer/authn/oidc/schema.go
Normal file
8
internal/proxy/director/layer/authn/oidc/schema.go
Normal file
@ -0,0 +1,8 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
8
internal/proxy/director/layer/authn/schema.go
Normal file
8
internal/proxy/director/layer/authn/schema.go
Normal file
@ -0,0 +1,8 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
17
internal/proxy/director/layer/authn/user.go
Normal file
17
internal/proxy/director/layer/authn/user.go
Normal file
@ -0,0 +1,17 @@
|
||||
package authn
|
||||
|
||||
type User struct {
|
||||
Subject string `json:"subject" expr:"subject"`
|
||||
Attrs map[string]any `json:"attrs" expr:"attrs"`
|
||||
}
|
||||
|
||||
func NewUser(subject string, attrs map[string]any) *User {
|
||||
if attrs == nil {
|
||||
attrs = make(map[string]any)
|
||||
}
|
||||
|
||||
return &User{
|
||||
Subject: subject,
|
||||
Attrs: attrs,
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/circuitbreaker-layer-options",
|
||||
"title": "Circuit breaker layer options",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchURLs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"authorizedCIDRs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"templateBlock": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
151
internal/proxy/director/layer/circuitbreaker/layer.go
Normal file
151
internal/proxy/director/layer/circuitbreaker/layer.go
Normal file
@ -0,0 +1,151 @@
|
||||
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,36 @@
|
||||
package circuitbreaker
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type LayerOptions struct {
|
||||
MatchURLs []string `mapstructure:"matchURLs"`
|
||||
AuthorizedCIDRs []string `mapstructure:"authorizedCIDRs"`
|
||||
TemplateBlock string `mapstructure:"templateBlock"`
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||
layerOptions := LayerOptions{
|
||||
MatchURLs: []string{"*"},
|
||||
AuthorizedCIDRs: []string{},
|
||||
TemplateBlock: "default",
|
||||
}
|
||||
|
||||
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
|
||||
}
|
19
internal/proxy/director/layer/circuitbreaker/options.go
Normal file
19
internal/proxy/director/layer/circuitbreaker/options.go
Normal file
@ -0,0 +1,19 @@
|
||||
package circuitbreaker
|
||||
|
||||
type Options struct {
|
||||
TemplateDir string
|
||||
}
|
||||
|
||||
type OptionFunc func(*Options)
|
||||
|
||||
func defaultOptions() *Options {
|
||||
return &Options{
|
||||
TemplateDir: "./templates",
|
||||
}
|
||||
}
|
||||
|
||||
func WithTemplateDir(templateDir string) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.TemplateDir = templateDir
|
||||
}
|
||||
}
|
8
internal/proxy/director/layer/circuitbreaker/schema.go
Normal file
8
internal/proxy/director/layer/circuitbreaker/schema.go
Normal file
@ -0,0 +1,8 @@
|
||||
package circuitbreaker
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed layer-options.json
|
||||
var RawLayerOptionsSchema []byte
|
73
internal/proxy/director/layer/queue/debouncer.go
Normal file
73
internal/proxy/director/layer/queue/debouncer.go
Normal file
@ -0,0 +1,73 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DebouncerMap struct {
|
||||
debouncers sync.Map
|
||||
}
|
||||
|
||||
func NewDebouncerMap() *DebouncerMap {
|
||||
return &DebouncerMap{
|
||||
debouncers: sync.Map{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DebouncerMap) Do(key string, after time.Duration, fn func()) {
|
||||
newDebouncer := NewDebouncer(after)
|
||||
rawDebouncer, loaded := m.debouncers.LoadOrStore(key, newDebouncer)
|
||||
|
||||
debouncer, ok := rawDebouncer.(*Debouncer)
|
||||
if !ok {
|
||||
panic(errors.Errorf("unexpected debouncer value, expected '%T', got '%T'", newDebouncer, rawDebouncer))
|
||||
}
|
||||
|
||||
if loaded {
|
||||
debouncer.Update(after)
|
||||
}
|
||||
|
||||
debouncer.Do(fn)
|
||||
}
|
||||
|
||||
func NewDebouncer(after time.Duration) *Debouncer {
|
||||
return &Debouncer{after: after}
|
||||
}
|
||||
|
||||
type Debouncer struct {
|
||||
mu sync.Mutex
|
||||
after time.Duration
|
||||
timer *time.Timer
|
||||
fn func()
|
||||
}
|
||||
|
||||
func (d *Debouncer) Do(fn func()) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.timer != nil {
|
||||
d.timer.Stop()
|
||||
}
|
||||
|
||||
d.fn = fn
|
||||
d.timer = time.AfterFunc(d.after, d.fn)
|
||||
}
|
||||
|
||||
func (d *Debouncer) Update(after time.Duration) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if after == d.after {
|
||||
return
|
||||
}
|
||||
|
||||
if d.timer != nil {
|
||||
d.timer.Stop()
|
||||
}
|
||||
|
||||
d.after = after
|
||||
d.timer = time.AfterFunc(d.after, d.fn)
|
||||
}
|
@ -11,15 +11,15 @@ import (
|
||||
|
||||
type LayerOptions struct {
|
||||
Capacity int64 `mapstructure:"capacity"`
|
||||
Matchers []string `mapstructure:"matchers"`
|
||||
KeepAlive time.Duration `mapstructure:"keepAlive"`
|
||||
MatchURLs []string `mapstructure:"matchURLs"`
|
||||
}
|
||||
|
||||
func fromStoreOptions(storeOptions store.LayerOptions, defaultKeepAlive time.Duration) (*LayerOptions, error) {
|
||||
layerOptions := LayerOptions{
|
||||
Capacity: 1000,
|
||||
Matchers: []string{"*"},
|
||||
KeepAlive: defaultKeepAlive,
|
||||
MatchURLs: []string{"*"},
|
||||
}
|
||||
|
||||
config := mapstructure.DecoderConfig{
|
||||
|
31
internal/proxy/director/layer/queue/metrics.go
Normal file
31
internal/proxy/director/layer/queue/metrics.go
Normal file
@ -0,0 +1,31 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const (
|
||||
metricNamespace = "bouncer_layer_queue"
|
||||
metricLabelProxy = "proxy"
|
||||
metricLabelLayer = "layer"
|
||||
)
|
||||
|
||||
var (
|
||||
metricQueueSessions = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "sessions",
|
||||
Help: "Bouncer's queue layer current sessions",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
metricQueueCapacity = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "capacity",
|
||||
Help: "Bouncer's queue layer capacity",
|
||||
Namespace: metricNamespace,
|
||||
},
|
||||
[]string{metricLabelProxy, metricLabelLayer},
|
||||
)
|
||||
)
|
@ -1,6 +1,8 @@
|
||||
package queue
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
TemplateDir string
|
||||
|
@ -4,18 +4,22 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"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/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
@ -30,7 +34,9 @@ type Queue struct {
|
||||
loadOnce sync.Once
|
||||
tmpl *template.Template
|
||||
|
||||
refreshJobRunning uint32
|
||||
refreshJobRunning uint32
|
||||
updateMetricsJobRunning uint32
|
||||
postKeepAliveDebouncer *DebouncerMap
|
||||
}
|
||||
|
||||
// LayerType implements director.MiddlewareLayer
|
||||
@ -52,6 +58,15 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return
|
||||
}
|
||||
|
||||
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
|
||||
if !matches {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer q.updateMetrics(ctx, layer.Proxy, layer.Name, options)
|
||||
|
||||
cookieName := q.getCookieName(layer.Name)
|
||||
|
||||
cookie, err := r.Cookie(cookieName)
|
||||
@ -72,8 +87,6 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
sessionID := cookie.Value
|
||||
queueName := string(layer.Name)
|
||||
|
||||
q.refreshQueue(queueName, options.KeepAlive)
|
||||
|
||||
rank, err := q.adapter.Touch(ctx, queueName, sessionID)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve session rank", logger.E(errors.WithStack(err)))
|
||||
@ -102,6 +115,30 @@ func (q *Queue) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) updateSessionsMetric(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName) {
|
||||
if !atomic.CompareAndSwapUint32(&q.updateMetricsJobRunning, 0, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
defer atomic.StoreUint32(&q.updateMetricsJobRunning, 0)
|
||||
|
||||
queueName := string(layerName)
|
||||
|
||||
status, err := q.adapter.Status(ctx, queueName)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve queue status", logger.E(errors.WithStack(err)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
metricQueueSessions.With(
|
||||
prometheus.Labels{
|
||||
metricLabelLayer: string(layerName),
|
||||
metricLabelProxy: string(proxyName),
|
||||
},
|
||||
).Set(float64(status.Sessions))
|
||||
}
|
||||
|
||||
func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueName string, options *LayerOptions, rank int64) {
|
||||
ctx := r.Context()
|
||||
|
||||
@ -135,22 +172,26 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
|
||||
return
|
||||
}
|
||||
|
||||
refreshRate := time.Duration(int64(options.KeepAlive.Seconds()/2)) * time.Second
|
||||
|
||||
templateData := struct {
|
||||
QueueName string
|
||||
LayerOptions *LayerOptions
|
||||
Rank int64
|
||||
CurrentSessions int64
|
||||
MaxSessions int64
|
||||
RefreshRate int64
|
||||
RefreshRate time.Duration
|
||||
}{
|
||||
QueueName: queueName,
|
||||
LayerOptions: options,
|
||||
Rank: rank + 1,
|
||||
CurrentSessions: status.Sessions,
|
||||
MaxSessions: options.Capacity,
|
||||
RefreshRate: 5,
|
||||
RefreshRate: refreshRate,
|
||||
}
|
||||
|
||||
w.Header().Add("Cache-Control", "no-cache")
|
||||
w.Header().Add("Retry-After", strconv.FormatInt(int64(refreshRate.Seconds()), 10))
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
|
||||
if err := q.tmpl.ExecuteTemplate(w, "queue", templateData); err != nil {
|
||||
@ -161,28 +202,59 @@ func (q *Queue) renderQueuePage(w http.ResponseWriter, r *http.Request, queueNam
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue) refreshQueue(queueName string, keepAlive time.Duration) {
|
||||
func (q *Queue) refreshQueue(ctx context.Context, layerName store.LayerName, keepAlive time.Duration) {
|
||||
if !atomic.CompareAndSwapUint32(&q.refreshJobRunning, 0, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer atomic.StoreUint32(&q.refreshJobRunning, 0)
|
||||
defer atomic.StoreUint32(&q.refreshJobRunning, 0)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), keepAlive*2)
|
||||
defer cancel()
|
||||
if err := q.adapter.Refresh(ctx, string(layerName), keepAlive); err != nil {
|
||||
logger.Error(ctx, "could not refresh queue",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("queue", layerName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.adapter.Refresh(ctx, queueName, keepAlive); err != nil {
|
||||
logger.Error(ctx, "could not refresh queue",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("queue", queueName),
|
||||
)
|
||||
}
|
||||
}()
|
||||
func (q *Queue) updateMetrics(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions) {
|
||||
// Update queue capacity metric
|
||||
metricQueueCapacity.With(
|
||||
prometheus.Labels{
|
||||
metricLabelLayer: string(layerName),
|
||||
metricLabelProxy: string(proxyName),
|
||||
},
|
||||
).Set(float64(options.Capacity))
|
||||
|
||||
// Refresh queue data and metrics
|
||||
q.refreshQueue(ctx, layerName, options.KeepAlive)
|
||||
q.updateSessionsMetric(ctx, proxyName, layerName)
|
||||
|
||||
// (Re)schedule an update job after session ttl + semi-random time padding
|
||||
// to update metrics after last session expiration
|
||||
randDuration := rand.Int63n(int64(options.KeepAlive))
|
||||
timePadding := options.KeepAlive/2 + time.Duration(randDuration)
|
||||
after := options.KeepAlive + timePadding
|
||||
|
||||
debouncingKey := fmt.Sprintf("%s/%s", proxyName, layerName)
|
||||
|
||||
q.postKeepAliveDebouncer.Do(debouncingKey, after, func() {
|
||||
ctx := logger.With(
|
||||
context.Background(),
|
||||
logger.F("proxy", proxyName),
|
||||
logger.F("layer", layerName),
|
||||
logger.F("after", after),
|
||||
)
|
||||
|
||||
logger.Info(ctx, "running post keep alive refresh job")
|
||||
|
||||
q.refreshQueue(ctx, layerName, options.KeepAlive)
|
||||
q.updateSessionsMetric(ctx, proxyName, layerName)
|
||||
})
|
||||
}
|
||||
|
||||
func (q *Queue) getCookieName(layerName store.LayerName) string {
|
||||
return fmt.Sprintf("_%s_%s", LayerType, layerName)
|
||||
return fmt.Sprintf("_bouncer_%s_%s", LayerType, layerName)
|
||||
}
|
||||
|
||||
func New(adapter Adapter, funcs ...OptionFunc) *Queue {
|
||||
@ -192,9 +264,10 @@ func New(adapter Adapter, funcs ...OptionFunc) *Queue {
|
||||
}
|
||||
|
||||
return &Queue{
|
||||
adapter: adapter,
|
||||
templateDir: opts.TemplateDir,
|
||||
defaultKeepAlive: opts.DefaultKeepAlive,
|
||||
adapter: adapter,
|
||||
templateDir: opts.TemplateDir,
|
||||
defaultKeepAlive: opts.DefaultKeepAlive,
|
||||
postKeepAliveDebouncer: NewDebouncerMap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ func (a *Adapter) Refresh(ctx context.Context, queueName string, keepAlive time.
|
||||
|
||||
cmd := tx.ZRangeByScore(ctx, lastSeenKey, &redis.ZRangeBy{
|
||||
Min: "0",
|
||||
Max: strconv.FormatInt(expires.Unix(), 10),
|
||||
Max: strconv.FormatInt(expires.UnixNano(), 10),
|
||||
})
|
||||
|
||||
members, err := cmd.Result()
|
||||
@ -75,7 +75,7 @@ func (a *Adapter) Touch(ctx context.Context, queueName string, sessionId string)
|
||||
|
||||
for retry > 0 {
|
||||
err := withTx(ctx, a.client, func(ctx context.Context, tx *redis.Tx) error {
|
||||
now := time.Now().UTC().Unix()
|
||||
now := time.Now().UTC().UnixNano()
|
||||
|
||||
err := tx.ZAddNX(ctx, rankKey, redis.Z{Score: float64(now), Member: sessionId}).Err()
|
||||
if err != nil {
|
||||
|
12
internal/proxy/director/layer/queue/redis/adapter_test.go
Normal file
12
internal/proxy/director/layer/queue/redis/adapter_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/queue/testsuite"
|
||||
)
|
||||
|
||||
func TestAdapter(t *testing.T) {
|
||||
adapter := NewAdapter(client, 3)
|
||||
testsuite.TestAdapter(t, adapter)
|
||||
}
|
58
internal/proxy/director/layer/queue/redis/main_test.go
Normal file
58
internal/proxy/director/layer/queue/redis/main_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var client redis.UniversalClient
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
// uses pool to try to connect to Docker
|
||||
err = pool.Client.Ping()
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
// pulls an image, creates a container based on it and runs it
|
||||
resource, err := pool.Run("redis", "alpine3.17", []string{})
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
client = redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: []string{resource.GetHostPort("6379/tcp")},
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if cmd := client.Ping(ctx); cmd.Err() != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(resource); err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user