Merge pull request 'Resources segregation by tenant' (#20) from tenant into master
arcad/emissary/pipeline/head There was a failure building this commit
Details
arcad/emissary/pipeline/head There was a failure building this commit
Details
Reviewed-on: #20
This commit is contained in:
commit
eee7e60a86
|
@ -10,6 +10,8 @@ dist/
|
||||||
/apps
|
/apps
|
||||||
/server-key.json
|
/server-key.json
|
||||||
/.emissary-token
|
/.emissary-token
|
||||||
|
/.emissary-admin-token
|
||||||
|
/.emissary-tenant
|
||||||
/out
|
/out
|
||||||
.mktools/
|
.mktools/
|
||||||
/CHANGELOG.md
|
/CHANGELOG.md
|
||||||
|
|
29
Makefile
29
Makefile
|
@ -15,6 +15,12 @@ OPENWRT_DEVICE ?= 192.168.1.1
|
||||||
watch: deps ## Watching updated files - live reload
|
watch: deps ## Watching updated files - live reload
|
||||||
( set -o allexport && source .env && set +o allexport && go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest )
|
( set -o allexport && source .env && set +o allexport && go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest )
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f .emissary-*
|
||||||
|
rm -f emissary.sqlite*
|
||||||
|
rm -f server-key.json
|
||||||
|
rm -f agent-key.json
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: test-go ## Executing tests
|
test: test-go ## Executing tests
|
||||||
|
|
||||||
|
@ -122,16 +128,25 @@ gitea-release: .mktools tools/gitea-release/bin/gitea-release.sh goreleaser chan
|
||||||
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
|
||||||
tools/gitea-release/bin/gitea-release.sh
|
tools/gitea-release/bin/gitea-release.sh
|
||||||
|
|
||||||
.emissary-token:
|
.emissary-tenant: .emissary-admin-token
|
||||||
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer --output .emissary-token"
|
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml client tenant create --token-file .emissary-admin-token --tenant-label Dev -f json | jq -r '.[0].id' > .emissary-tenant"
|
||||||
|
|
||||||
|
.emissary-admin-token:
|
||||||
|
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role admin --output .emissary-admin-token"
|
||||||
|
|
||||||
|
.emissary-token: .emissary-tenant
|
||||||
|
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer --output .emissary-token --tenant $(shell cat .emissary-tenant)"
|
||||||
|
|
||||||
AGENT_ID ?= 1
|
AGENT_ID ?= 1
|
||||||
|
|
||||||
load-sample-specs:
|
claim-agent: .emissary-token
|
||||||
cat misc/spec-samples/app.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.emissary.cadoles.com
|
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml client agent claim --agent-thumbprint $(shell go run ./cmd/agent agent show-thumbprint)"
|
||||||
cat misc/spec-samples/proxy.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com
|
|
||||||
cat misc/spec-samples/mdns.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.emissary.cadoles.com
|
load-sample-specs: .emissary-token
|
||||||
cat misc/spec-samples/uci.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
cat misc/spec-samples/app.emissary.cadoles.com.json | $(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml client agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.emissary.cadoles.com"
|
||||||
|
cat misc/spec-samples/proxy.emissary.cadoles.com.json | $(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml client agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com"
|
||||||
|
cat misc/spec-samples/mdns.emissary.cadoles.com.json | $(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml client agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.emissary.cadoles.com"
|
||||||
|
cat misc/spec-samples/uci.emissary.cadoles.com.json | $(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml client agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name uci.emissary.cadoles.com"
|
||||||
|
|
||||||
version: .mktools
|
version: .mktools
|
||||||
@echo $(MKT_PROJECT_VERSION)
|
@echo $(MKT_PROJECT_VERSION)
|
||||||
|
|
34
README.md
34
README.md
|
@ -6,6 +6,40 @@ Control plane for "edge" (and OpenWRT-based) devices.
|
||||||
|
|
||||||
> ⚠ Emissary is currently in a very alpha stage ! Expect breaking changes...
|
> ⚠ Emissary is currently in a very alpha stage ! Expect breaking changes...
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
**Dependencies**
|
||||||
|
- [Go >= 1.21](https://go.dev/)
|
||||||
|
- `GNU Make`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Start server and a local agent
|
||||||
|
make watch
|
||||||
|
|
||||||
|
# In an other terminal
|
||||||
|
|
||||||
|
# Create an admin token
|
||||||
|
make .emissary-admin-token
|
||||||
|
|
||||||
|
# Create a new tenant
|
||||||
|
make .emissary-tenant
|
||||||
|
|
||||||
|
# Create a new writer token for this tenant
|
||||||
|
make .emissary-token
|
||||||
|
|
||||||
|
# Claim the agent for your newly created tenant
|
||||||
|
make claim-agent
|
||||||
|
|
||||||
|
# Query your agents
|
||||||
|
./bin/server client agent query
|
||||||
|
|
||||||
|
# Load sample specs for your agent
|
||||||
|
make load-sample-specs
|
||||||
|
|
||||||
|
## Optional: reset your workspace
|
||||||
|
make clean
|
||||||
|
```
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Manually
|
### Manually
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command"
|
"forge.cadoles.com/Cadoles/emissary/internal/command"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client"
|
||||||
|
|
||||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
|
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
|
||||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
|
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
|
||||||
|
@ -20,5 +20,5 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, agent.Root(), api.Root())
|
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, agent.Root(), client.Root())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command"
|
"forge.cadoles.com/Cadoles/emissary/internal/command"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/server"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/server"
|
||||||
|
|
||||||
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
|
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
|
||||||
|
@ -21,5 +21,5 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, server.Root(), api.Root())
|
command.Main(BuildDate, ProjectVersion, GitRef, DefaultConfigPath, server.Root(), client.Root())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
- (FR) - [Introduction](./fr/introduction.md)
|
- (FR) - [Vue d'ensemble](./others/fr/overview.md)
|
||||||
|
- (FR) - [Authentification et autorisation](./others/fr/auth.md)
|
||||||
|
|
||||||
## Tutorials
|
## Tutorials
|
||||||
|
|
||||||
|
@ -9,15 +10,13 @@
|
||||||
- (FR) - [Déployer une configuration UCI personnalisée sur un agent](./tutorials/fr/deploy-uci-configuration.md)
|
- (FR) - [Déployer une configuration UCI personnalisée sur un agent](./tutorials/fr/deploy-uci-configuration.md)
|
||||||
- (FR) - [Démarrer un agent avec Docker](./tutorials/fr/docker-agent.md)
|
- (FR) - [Démarrer un agent avec Docker](./tutorials/fr/docker-agent.md)
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
### Specifications
|
### Specifications
|
||||||
|
|
||||||
- [Schéma `app.emissary.cadoles.com`](../internal/agent/controller/app/spec/schema.json)
|
- [Schema `app.emissary.cadoles.com`](../internal/agent/controller/app/spec/schema.json)
|
||||||
- [Schéma `proxy.emissary.cadoles.com`](../internal/spec/proxy/schema.json)
|
- [Schema `proxy.emissary.cadoles.com`](../internal/spec/proxy/schema.json)
|
||||||
- [Schéma `mdns.emissary.cadoles.com`](../internal/agent/controller/mdns/spec/schema.json)
|
- [Schema `mdns.emissary.cadoles.com`](../internal/agent/controller/mdns/spec/schema.json)
|
||||||
- [Schéma `uci.emissary.cadoles.com`](../internal/spec/uci/schema.json)
|
- [Schema `uci.emissary.cadoles.com`](../internal/spec/uci/schema.json)
|
||||||
- [Schéma `sysupgrade.openwrt.emissary.cadoles.com`](../internal/agent/controller/openwrt/spec/sysupgrade/schema.json)
|
- [Schema `sysupgrade.openwrt.emissary.cadoles.com`](../internal/agent/controller/openwrt/spec/sysupgrade/schema.json)
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Authentification et autorisation
|
||||||
|
|
||||||
|
## Authentification
|
||||||
|
|
||||||
|
Emissary utilise des [**JSON Web Token**](https://fr.wikipedia.org/wiki/JSON_Web_Token) (JWT) afin d'authentifier les appels à son API REST.
|
||||||
|
|
||||||
|
L'implémentation est compatible avec tout serveur d'authentification exposant une URL proposant un [**JSON Web Key Set**](https://www.ory.sh/docs/hydra/jwks#the-role-of-well-knownjwksjson).
|
||||||
|
|
||||||
|
La plupart des serveurs OpenID Connect exposent un point d'entrée du type [`/.well-known/jwks.json`](https://www.ory.sh/docs/hydra/jwks#the-role-of-well-knownjwksjson) remplissant ce rôle.
|
||||||
|
|
||||||
|
Emissary est également en capacité à fonctionner en mode autonome en générant des JWTs signés par une clé privée locale.
|
||||||
|
|
||||||
|
## Ségrégation des ressources
|
||||||
|
|
||||||
|
Emissary suit une stratégie ["multitenant"](https://fr.wikipedia.org/wiki/Multitenant) de séparer les ressources par organisation.
|
||||||
|
|
||||||
|
Un utilisateur est obligatoirement associé à un `tenant`` et ne peut opérer que sur les ressources associées à celui ci.
|
||||||
|
|
||||||
|
## Autorisation
|
||||||
|
|
||||||
|
Au sein d'un `tenant`, un utilisateur peut avoir un des rôles suivants:
|
||||||
|
|
||||||
|
- `writer` - Autorisé à visualiser et modifier les ressources;
|
||||||
|
- `reader` - Autorisé à visualiser les ressources.
|
||||||
|
|
||||||
|
Un rôle spécial `admin` permet la création et la suppression de `tenants`.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Introduction
|
# Vue d'ensemble
|
||||||
|
|
||||||
"Emissary" est un programme entrant dans la catégorie des outils de gestion et déploiement de configuration.
|
"Emissary" est un programme entrant dans la catégorie des outils de gestion et déploiement de configuration.
|
||||||
|
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
@ -80,13 +80,13 @@ Via la spécification [`uci.emissary.cadoles.com`](../../../internal/spec/uci/sc
|
||||||
AGENT_THUMBPRINT="<empreinte agent>"
|
AGENT_THUMBPRINT="<empreinte agent>"
|
||||||
|
|
||||||
# Récupérer l'identifiant de l'agent
|
# Récupérer l'identifiant de l'agent
|
||||||
AGENT_ID=$(emissary api agent query -f json | jq -r --arg thumbprint "$AGENT_THUMBPRINT" '.[] | select(.thumbprint == $thumbprint) | .id')
|
AGENT_ID=$(emissary client agent query -f json | jq -r --arg thumbprint "$AGENT_THUMBPRINT" '.[] | select(.thumbprint == $thumbprint) | .id')
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Assigner la spécification à l'agent UCI:
|
2. Assigner la spécification à l'agent UCI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat my-uci-spec.json | emissary api agent spec update -a ${AGENT_ID} --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
cat my-uci-spec.json | emissary client agent spec update -a ${AGENT_ID} --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bravo, vous avez déployé des spécifications UCI sur votre agent !**
|
**Bravo, vous avez déployé des spécifications UCI sur votre agent !**
|
||||||
|
@ -112,7 +112,7 @@ En intervenant directement sur notre spécification, il est possible de modifier
|
||||||
2. Mettre à jour la configuration de l'agent:
|
2. Mettre à jour la configuration de l'agent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat my-uci-spec.json | emissary api agent spec update -a ${AGENT_ID} --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
cat my-uci-spec.json | emissary client agent spec update -a ${AGENT_ID} --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Sur l'agent, après quelques secondes (par défaut, la fréquence de mise à jour est de 1 fois par minute) l'agent devrait avoir son `hostname` mis à jour:
|
3. Sur l'agent, après quelques secondes (par défaut, la fréquence de mise à jour est de 1 fois par minute) l'agent devrait avoir son `hostname` mis à jour:
|
||||||
|
|
|
@ -80,15 +80,31 @@
|
||||||
5. Créer un jeton d'administration:
|
5. Créer un jeton d'administration:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo emissary --workdir /usr/share/emissary --config /etc/emissary/server.yml server auth create-token --role writer --subject $(whoami)
|
sudo emissary --workdir /usr/share/emissary --config /etc/emissary/server.yml server auth create-token --role admin -o "$HOME/.config/emissary/admin-token"
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note** Le jeton sera stocké dans le répertoire `$HOME/.config/emissary`.
|
> **Note** Le jeton sera stocké dans le répertoire `$HOME/.config/emissary`.
|
||||||
|
|
||||||
6. Vérifier l'authentification sur l'API:
|
6. Créer un nouveau `tenant`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
emissary api agent query
|
sudo emissary --workdir /usr/share/emissary --config /etc/emissary/server.yml client tenant create --tenant-label "My Tenant" -o wide --token-file "$HOME/.config/emissary/admin-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
Noter la valeur de l'UUID (de la forme `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) affiché dans la colonne `ID`. Il sera identifié comme `$EMISSARY_TENANT` dans les étapes suivantes.
|
||||||
|
|
||||||
|
7. Créer un jeton d'authentification pour ce nouveau tenant:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo emissary --workdir /usr/share/emissary --config /etc/emissary/server.yml server auth create-token --role writer --tenant $EMISSARY_TENANT
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note** Le jeton sera stocké dans le fichier `$HOME/.config/emissary/auth-token`. Il sera le jeton utilisé par défaut par le CLI Emissary.
|
||||||
|
|
||||||
|
8. Vérifier l'authentification sur l'API:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
emissary client agent query
|
||||||
```
|
```
|
||||||
|
|
||||||
Une réponse équivalente à la suivante devrait s'afficher:
|
Une réponse équivalente à la suivante devrait s'afficher:
|
||||||
|
@ -128,10 +144,18 @@
|
||||||
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.680 [INFO] <./internal/agent/controller/openwrt/sysupgrade_controller.go:36> (*SysUpgradeController).Reconcile could not find sysupgrade spec, doing nothing {"controller": "sysupgrade-controller"}
|
Thu May 25 18:48:51 2023 daemon.info emissary[2202]: 2023-05-25 18:48:51.680 [INFO] <./internal/agent/controller/openwrt/sysupgrade_controller.go:36> (*SysUpgradeController).Reconcile could not find sysupgrade spec, doing nothing {"controller": "sysupgrade-controller"}
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Sur le serveur, vérifier que l'agent a pu s'enregistrer:
|
2. Récupérer le `thumbprint` de votre agent:
|
||||||
|
|
||||||
|
```
|
||||||
|
emissary agent show-thumbprint
|
||||||
|
```
|
||||||
|
|
||||||
|
Noter la valeur de la chaîne de caractères affichée. Elle sera identifiée comme `$AGENT_THUMBPRINT` dans les étapes suivantes.
|
||||||
|
|
||||||
|
3. Sur le serveur, "réclamer" votre agent:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
emissary api agent query
|
emissary client agent claim --agent-thumbprint $AGENT_THUMBPRINT
|
||||||
```
|
```
|
||||||
|
|
||||||
Un message de ce type devrait s'afficher:
|
Un message de ce type devrait s'afficher:
|
||||||
|
@ -144,12 +168,12 @@
|
||||||
+----+-------+-----------------------------------+--------+-----------------------------------+-----------------------------------+
|
+----+-------+-----------------------------------+--------+-----------------------------------+-----------------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
Noter l'identifiant associé à l'agent.
|
Noter la valeur de l'identifiant affiché dans la colonne `ID`. Il sera identifié comme `$AGENT_ID` dans les étapes suivantes.
|
||||||
|
|
||||||
4. Mettre à jour le statut de l'agent afin qu'il soit en capacité à récupérer ses spécifications:
|
4. Mettre à jour le statut de l'agent afin qu'il soit en capacité à récupérer ses spécifications:
|
||||||
|
|
||||||
```
|
```
|
||||||
emissary api agent update --agent-id <agent_id> --status 1
|
emissary client agent update --agent-id $AGENT_ID --status 1
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bravo, vous avez appairé votre premier agent et son serveur Emissary !**
|
**Bravo, vous avez appairé votre premier agent et son serveur Emissary !**
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -28,7 +28,7 @@ require (
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/qri-io/jsonschema v0.2.1
|
github.com/qri-io/jsonschema v0.2.1
|
||||||
github.com/urfave/cli/v2 v2.26.0
|
github.com/urfave/cli/v2 v2.26.0
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07
|
gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.21.0
|
modernc.org/sqlite v1.21.0
|
||||||
)
|
)
|
||||||
|
@ -78,7 +78,7 @@ require (
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
@ -123,4 +123,4 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace forge.cadoles.com/arcad/edge => ../edge
|
// replace forge.cadoles.com/arcad/edge => ../edge
|
||||||
replace github.com/allegro/bigcache/v3 v3.1.0 => github.com/Bornholm/bigcache v0.0.0-20231201111725-1ddf51584cad
|
// replace gitlab.com/wpetit/goweb => ../goweb
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -702,6 +702,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||||
|
@ -1317,6 +1319,8 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt
|
||||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07 h1:0V95X1cBpdj5zyOe6oGtn/BQHlRpV8WlL3eTs3jaxiA=
|
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07 h1:0V95X1cBpdj5zyOe6oGtn/BQHlRpV8WlL3eTs3jaxiA=
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07/go.mod h1:Nfr7aZPiSN6biFumhiHbh9k8A3rKQRzR+o0bVtv78UY=
|
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07/go.mod h1:Nfr7aZPiSN6biFumhiHbh9k8A3rKQRzR+o0bVtv78UY=
|
||||||
|
gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88 h1:dsyRrmhp7fl/YaY1YIzz7lm9qfIFI5KpKNbXwuhTULA=
|
||||||
|
gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88/go.mod h1:bg+TN16Rq2ygLQbB4VDSHQFNouAEzcy3AAutStehllA=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/lestrrat-go/jwx/v2/jws"
|
"github.com/lestrrat-go/jwx/v2/jws"
|
||||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultAcceptableSkew = 5 * time.Minute
|
const DefaultAcceptableSkew = 5 * time.Minute
|
||||||
|
@ -34,17 +35,19 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||||
|
|
||||||
token, err := jwt.Parse([]byte(rawToken), jwt.WithVerify(false))
|
token, err := jwt.Parse([]byte(rawToken), jwt.WithVerify(false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
logger.Debug(ctx, "could not parse jwt token", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawThumbprint, exists := token.Get(keyThumbprint)
|
rawThumbprint, exists := token.Get(keyThumbprint)
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, errors.Errorf("could not find '%s' claim", keyThumbprint)
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbrint, ok := rawThumbprint.(string)
|
thumbrint, ok := rawThumbprint.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Errorf("unexpected '%s' claim value: '%v'", keyThumbprint, rawThumbprint)
|
logger.Debug(ctx, "unexpected claim value", logger.F("claim", rawThumbprint), logger.F("value", rawThumbprint))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
agents, _, err := a.repo.Query(
|
agents, _, err := a.repo.Query(
|
||||||
|
@ -57,7 +60,8 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(agents) != 1 {
|
if len(agents) != 1 {
|
||||||
return nil, errors.Errorf("unexpected number of found agents: '%d'", len(agents))
|
logger.Debug(ctx, "unexpected number of found agents", logger.F("total", len(agents)))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
agent, err := a.repo.Get(
|
agent, err := a.repo.Get(
|
||||||
|
@ -75,14 +79,15 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||||
jwt.WithAcceptableSkew(a.acceptableSkew),
|
jwt.WithAcceptableSkew(a.acceptableSkew),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
logger.Error(ctx, "could not parse jwt", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
contactedAt := time.Now()
|
contactedAt := time.Now().UTC()
|
||||||
|
|
||||||
agent, err = a.repo.Update(ctx, agent.ID, datastore.WithAgentUpdateContactedAt(contactedAt))
|
agent, err = a.repo.Update(ctx, agent.ID, datastore.WithAgentUpdateContactedAt(contactedAt))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
user := &User{
|
||||||
|
|
|
@ -16,6 +16,15 @@ func (u *User) Subject() string {
|
||||||
return fmt.Sprintf("agent-%d", u.agent.ID)
|
return fmt.Sprintf("agent-%d", u.agent.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subject implements auth.User
|
||||||
|
func (u *User) Tenant() datastore.TenantID {
|
||||||
|
if u.agent.TenantID == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return *u.agent.TenantID
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) Agent() *datastore.Agent {
|
func (u *User) Agent() *datastore.Agent {
|
||||||
return u.agent
|
return u.agent
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import "github.com/pkg/errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnauthenticated = errors.New("unauthenticated")
|
||||||
|
ErrUnauthorized = errors.New(("unauthorized"))
|
||||||
|
)
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/api"
|
"gitlab.com/wpetit/goweb/api"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
@ -29,10 +30,9 @@ func CtxUser(ctx context.Context) (User, error) {
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrUnauthenticated = errors.New("unauthenticated")
|
|
||||||
|
|
||||||
type User interface {
|
type User interface {
|
||||||
Subject() string
|
Subject() string
|
||||||
|
Tenant() datastore.TenantID
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authenticator interface {
|
type Authenticator interface {
|
||||||
|
@ -49,11 +49,12 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
for _, auth := range authenticators {
|
for _, auth := range authenticators {
|
||||||
user, err = auth.Authenticate(ctx, r)
|
user, err = auth.Authenticate(ctx, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(ctx, "could not authenticate request", logger.E(errors.WithStack(err)))
|
errs = append(errs, errors.WithStack(err))
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,9 +64,22 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
api.ErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, nil)
|
isUnauthorized, isUnauthenticated, isUnknown := checkErrors(errs)
|
||||||
|
|
||||||
return
|
switch {
|
||||||
|
case isUnauthorized && !isUnknown:
|
||||||
|
api.ErrorResponse(w, http.StatusForbidden, api.ErrCodeForbidden, nil)
|
||||||
|
return
|
||||||
|
case isUnauthenticated && !isUnknown:
|
||||||
|
api.ErrorResponse(w, http.StatusForbidden, api.ErrCodeForbidden, nil)
|
||||||
|
return
|
||||||
|
case isUnknown:
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
api.ErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = logger.With(ctx, logger.F("user", user.Subject()))
|
ctx = logger.With(ctx, logger.F("user", user.Subject()))
|
||||||
|
@ -77,3 +91,24 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
|
||||||
return http.HandlerFunc(fn)
|
return http.HandlerFunc(fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkErrors(errs []error) (isUnauthorized bool, isUnauthenticated bool, isUnknown bool) {
|
||||||
|
isUnauthenticated = false
|
||||||
|
isUnauthorized = false
|
||||||
|
isUnknown = false
|
||||||
|
|
||||||
|
for _, e := range errs {
|
||||||
|
switch {
|
||||||
|
case errors.Is(e, ErrUnauthorized):
|
||||||
|
isUnauthorized = true
|
||||||
|
continue
|
||||||
|
case errors.Is(e, ErrUnauthenticated):
|
||||||
|
isUnauthenticated = true
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
isUnknown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
package thirdparty
|
|
||||||
|
|
||||||
import "forge.cadoles.com/Cadoles/emissary/internal/auth"
|
|
||||||
|
|
||||||
type Role string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RoleWriter Role = "writer"
|
|
||||||
RoleReader Role = "reader"
|
|
||||||
)
|
|
||||||
|
|
||||||
func isValidRole(r string) bool {
|
|
||||||
rr := Role(r)
|
|
||||||
|
|
||||||
return rr == RoleWriter || rr == RoleReader
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
subject string
|
|
||||||
role Role
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subject implements auth.User
|
|
||||||
func (u *User) Subject() string {
|
|
||||||
return u.subject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Role() Role {
|
|
||||||
return u.role
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ auth.User = &User{}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package thirdparty
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -7,21 +7,25 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultAcceptableSkew = 5 * time.Minute
|
const DefaultAcceptableSkew = 5 * time.Minute
|
||||||
|
|
||||||
type (
|
type (
|
||||||
GetKeySet func(context.Context) (jwk.Set, error)
|
GetKeySet func(context.Context) (jwk.Set, error)
|
||||||
GetTokenRole func(context.Context, jwt.Token) (string, error)
|
GetTokenRole func(context.Context, jwt.Token) (string, error)
|
||||||
|
GetTokenTenant func(context.Context, jwt.Token) (string, error)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
getKeySet GetKeySet
|
getKeySet GetKeySet
|
||||||
getTokenRole GetTokenRole
|
getTokenRole GetTokenRole
|
||||||
|
getTokenTenant GetTokenTenant
|
||||||
acceptableSkew time.Duration
|
acceptableSkew time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,29 +48,45 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||||
|
|
||||||
token, err := parseToken(ctx, keys, rawToken, a.acceptableSkew)
|
token, err := parseToken(ctx, keys, rawToken, a.acceptableSkew)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
logger.Debug(ctx, "could not parse jwt token", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawRole, err := a.getTokenRole(ctx, token)
|
rawRole, err := a.getTokenRole(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
logger.Debug(ctx, "could not retrieve token role", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isValidRole(rawRole) {
|
if !isValidRole(rawRole) {
|
||||||
return nil, errors.Errorf("invalid role '%s'", rawRole)
|
return nil, errors.WithStack(auth.ErrUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawTenantID, err := a.getTokenTenant(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(ctx, "could not retrieve token tenant", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID, err := datastore.ParseTenantID(rawTenantID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(ctx, "could not retrieve token tenant", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
user := &User{
|
||||||
subject: token.Subject(),
|
subject: token.Subject(),
|
||||||
role: Role(rawRole),
|
role: Role(rawRole),
|
||||||
|
tenantID: tenantID,
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthenticator(getKeySet GetKeySet, getTokenRole GetTokenRole, acceptableSkew time.Duration) *Authenticator {
|
func NewAuthenticator(getKeySet GetKeySet, getTokenRole GetTokenRole, getTokenTenant GetTokenTenant, acceptableSkew time.Duration) *Authenticator {
|
||||||
return &Authenticator{
|
return &Authenticator{
|
||||||
getTokenRole: getTokenRole,
|
getTokenRole: getTokenRole,
|
||||||
|
getTokenTenant: getTokenTenant,
|
||||||
getKeySet: getKeySet,
|
getKeySet: getKeySet,
|
||||||
acceptableSkew: acceptableSkew,
|
acceptableSkew: acceptableSkew,
|
||||||
}
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
package thirdparty
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
"github.com/lestrrat-go/jwx/v2/jws"
|
"github.com/lestrrat-go/jwx/v2/jws"
|
||||||
|
@ -26,9 +27,12 @@ func parseToken(ctx context.Context, keys jwk.Set, rawToken string, acceptableSk
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultRoleKey string = "role"
|
const (
|
||||||
|
DefaultRoleKey string = "role"
|
||||||
|
DefaultTenantKey string = "tenant"
|
||||||
|
)
|
||||||
|
|
||||||
func GenerateToken(ctx context.Context, key jwk.Key, subject string, role Role) (string, error) {
|
func GenerateToken(ctx context.Context, key jwk.Key, tenant datastore.TenantID, subject string, role Role) (string, error) {
|
||||||
token := jwt.New()
|
token := jwt.New()
|
||||||
|
|
||||||
if err := token.Set(jwt.SubjectKey, subject); err != nil {
|
if err := token.Set(jwt.SubjectKey, subject); err != nil {
|
||||||
|
@ -39,6 +43,10 @@ func GenerateToken(ctx context.Context, key jwk.Key, subject string, role Role)
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := token.Set(DefaultTenantKey, tenant); err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
|
@ -0,0 +1,42 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleWriter Role = "writer"
|
||||||
|
RoleReader Role = "reader"
|
||||||
|
RoleAdmin Role = "admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isValidRole(r string) bool {
|
||||||
|
rr := Role(r)
|
||||||
|
|
||||||
|
return rr == RoleWriter || rr == RoleReader || rr == RoleAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
subject string
|
||||||
|
tenantID datastore.TenantID
|
||||||
|
role Role
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject implements auth.User
|
||||||
|
func (u *User) Subject() string {
|
||||||
|
return u.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenant implements auth.User
|
||||||
|
func (u *User) Tenant() datastore.TenantID {
|
||||||
|
return u.tenantID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Role() Role {
|
||||||
|
return u.role
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ auth.User = &User{}
|
|
@ -104,6 +104,10 @@ func RunCommand() *cli.Command {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.SetLevel(logger.LevelInfo)
|
||||||
|
logger.Info(ctx.Context, "agent thumbprint", logger.F("thumbprint", thumbprint))
|
||||||
|
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||||
|
|
||||||
collectors := createShellCollectors(&conf.Agent)
|
collectors := createShellCollectors(&conf.Agent)
|
||||||
collectors = append(collectors, buildinfo.NewCollector())
|
collectors = append(collectors, buildinfo.NewCollector())
|
||||||
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/agent"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Root() *cli.Command {
|
|
||||||
return &cli.Command{
|
|
||||||
Name: "api",
|
|
||||||
Usage: "API related commands",
|
|
||||||
Subcommands: []*cli.Command{
|
|
||||||
agent.Root(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClaimCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "claim",
|
||||||
|
Usage: "Claim agent",
|
||||||
|
Flags: clientFlag.ComposeFlags(
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "agent-thumbprint",
|
||||||
|
Value: "",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
|
|
||||||
|
token, err := clientFlag.GetToken(baseFlags)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
agentThumbprint := ctx.String("agent-thumbprint")
|
||||||
|
|
||||||
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
|
agent, err := client.ClaimAgent(ctx.Context, agentThumbprint)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := agentHints(baseFlags.OutputMode)
|
||||||
|
|
||||||
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,12 @@ package agent
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CountCommand() *cli.Command {
|
func CountCommand() *cli.Command {
|
|
@ -3,14 +3,14 @@ package agent
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DeleteCommand() *cli.Command {
|
func DeleteCommand() *cli.Command {
|
|
@ -3,7 +3,7 @@ package flag
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
|
@ -3,13 +3,13 @@ package agent
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCommand() *cli.Command {
|
func GetCommand() *cli.Command {
|
|
@ -1,6 +1,9 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import "forge.cadoles.com/Cadoles/emissary/internal/format"
|
import (
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format/table"
|
||||||
|
)
|
||||||
|
|
||||||
func agentHints(outputMode format.OutputMode) format.Hints {
|
func agentHints(outputMode format.OutputMode) format.Hints {
|
||||||
return format.Hints{
|
return format.Hints{
|
||||||
|
@ -10,8 +13,8 @@ func agentHints(outputMode format.OutputMode) format.Hints {
|
||||||
format.NewProp("Label", "Label"),
|
format.NewProp("Label", "Label"),
|
||||||
format.NewProp("Thumbprint", "Thumbprint"),
|
format.NewProp("Thumbprint", "Thumbprint"),
|
||||||
format.NewProp("Status", "Status"),
|
format.NewProp("Status", "Status"),
|
||||||
format.NewProp("ContactedAt", "ContactedAt"),
|
format.NewProp("ContactedAt", "ContactedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||||
format.NewProp("UpdatedAt", "UpdatedAt"),
|
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,13 +3,13 @@ package agent
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func QueryCommand() *cli.Command {
|
func QueryCommand() *cli.Command {
|
|
@ -1,7 +1,7 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/spec"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ func Root() *cli.Command {
|
||||||
UpdateCommand(),
|
UpdateCommand(),
|
||||||
GetCommand(),
|
GetCommand(),
|
||||||
DeleteCommand(),
|
DeleteCommand(),
|
||||||
|
ClaimCommand(),
|
||||||
spec.Root(),
|
spec.Root(),
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -3,14 +3,14 @@ package spec
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DeleteCommand() *cli.Command {
|
func DeleteCommand() *cli.Command {
|
|
@ -3,13 +3,13 @@ package spec
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetCommand() *cli.Command {
|
func GetCommand() *cli.Command {
|
|
@ -4,15 +4,15 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateCommand() *cli.Command {
|
func UpdateCommand() *cli.Command {
|
|
@ -3,13 +3,13 @@ package agent
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdateCommand() *cli.Command {
|
func UpdateCommand() *cli.Command {
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format/table"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format/table"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
|
@ -0,0 +1,18 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/agent"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/tenant"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Root() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "client",
|
||||||
|
Usage: "API client related commands",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
agent.Root(),
|
||||||
|
tenant.Root(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package tenant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "Create tenant",
|
||||||
|
Flags: clientFlag.ComposeFlags(
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "tenant-label",
|
||||||
|
Usage: "Set `TENANT_LABEL` to targeted tenant",
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
|
|
||||||
|
token, err := clientFlag.GetToken(baseFlags)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantLabel := ctx.String("tenant-label")
|
||||||
|
|
||||||
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
|
agent, err := client.CreateTenant(ctx.Context, tenantLabel)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := tenantHints(baseFlags.OutputMode)
|
||||||
|
|
||||||
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package tenant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
|
tenantFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/tenant/flag"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Usage: "Delete tenant",
|
||||||
|
Flags: tenantFlag.WithTenantFlags(),
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
|
|
||||||
|
token, err := clientFlag.GetToken(baseFlags)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID, err := tenantFlag.AssertTenantID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
|
tenantID, err = client.DeleteTenant(ctx.Context, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := format.Hints{
|
||||||
|
OutputMode: baseFlags.OutputMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, struct {
|
||||||
|
ID datastore.TenantID `json:"id"`
|
||||||
|
}{
|
||||||
|
ID: tenantID,
|
||||||
|
}); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import (
|
||||||
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithTenantFlags(flags ...cli.Flag) []cli.Flag {
|
||||||
|
baseFlags := clientFlag.ComposeFlags(
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "tenant-id",
|
||||||
|
Usage: "use `TENANT_ID` as targeted tenant",
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
flags = append(flags, baseFlags...)
|
||||||
|
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertTenantID(ctx *cli.Context) (datastore.TenantID, error) {
|
||||||
|
rawTenantID := ctx.String("tenant-id")
|
||||||
|
|
||||||
|
if rawTenantID == "" {
|
||||||
|
return "", errors.New("flag 'tenant-id' is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID, err := datastore.ParseTenantID(rawTenantID)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenantID, nil
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package tenant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
|
tenantFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/tenant/flag"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "get",
|
||||||
|
Usage: "Get tenant",
|
||||||
|
Flags: tenantFlag.WithTenantFlags(),
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
|
|
||||||
|
token, err := clientFlag.GetToken(baseFlags)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID, err := tenantFlag.AssertTenantID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
|
agent, err := client.GetTenant(ctx.Context, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := tenantHints(baseFlags.OutputMode)
|
||||||
|
|
||||||
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package tenant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format/table"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tenantHints(outputMode format.OutputMode) format.Hints {
|
||||||
|
return format.Hints{
|
||||||
|
OutputMode: outputMode,
|
||||||
|
Props: []format.Prop{
|
||||||
|
format.NewProp("ID", "ID", table.WithCompactModeMaxColumnWidth(8)),
|
||||||
|
format.NewProp("Label", "Label"),
|
||||||
|
format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||||
|
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package tenant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func QueryCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "query",
|
||||||
|
Usage: "Query tenants",
|
||||||
|
Flags: clientFlag.ComposeFlags(
|
||||||
|
&cli.Int64SliceFlag{
|
||||||
|
Name: "ids",
|
||||||
|
Usage: "use `IDS` as query filter",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
|
|
||||||
|
token, err := clientFlag.GetToken(baseFlags)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]client.QueryTenantsOptionFunc, 0)
|
||||||
|
|
||||||
|
rawIDs := ctx.StringSlice("ids")
|
||||||
|
if rawIDs != nil {
|
||||||
|
tenantIDs := func(ids []string) []datastore.TenantID {
|
||||||
|
tenantIDs := make([]datastore.TenantID, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
tenantIDs[i] = datastore.TenantID(id)
|
||||||
|
}
|
||||||
|
return tenantIDs
|
||||||
|
}(rawIDs)
|
||||||
|
options = append(options, client.WithQueryTenantsID(tenantIDs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
|
tenants, _, err := client.QueryTenants(ctx.Context, options...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := tenantHints(baseFlags.OutputMode)
|
||||||
|
|
||||||
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(tenants)...); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package tenant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Root() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "tenant",
|
||||||
|
Usage: "Tenants related commands",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
CreateCommand(),
|
||||||
|
GetCommand(),
|
||||||
|
UpdateCommand(),
|
||||||
|
DeleteCommand(),
|
||||||
|
QueryCommand(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package tenant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
|
tenantFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/tenant/flag"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "update",
|
||||||
|
Usage: "Update tenant",
|
||||||
|
Flags: tenantFlag.WithTenantFlags(
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "tenant-label",
|
||||||
|
Usage: "Set `TENANT_LABEL` to targeted tenant",
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
|
|
||||||
|
token, err := clientFlag.GetToken(baseFlags)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID, err := tenantFlag.AssertTenantID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]client.UpdateTenantOptionFunc, 0)
|
||||||
|
|
||||||
|
label := ctx.String("tenant-label")
|
||||||
|
if label != "" {
|
||||||
|
options = append(options, client.WithTenantLabel(label))
|
||||||
|
}
|
||||||
|
|
||||||
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
|
agent, err := client.UpdateTenant(ctx.Context, tenantID, options...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := tenantHints(baseFlags.OutputMode)
|
||||||
|
|
||||||
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,12 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
"github.com/lithammer/shortuuid/v4"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
@ -21,13 +22,18 @@ func CreateTokenCommand() *cli.Command {
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "role",
|
Name: "role",
|
||||||
Usage: fmt.Sprintf("associate `ROLE` to the token (available: %v)", []thirdparty.Role{thirdparty.RoleReader, thirdparty.RoleWriter}),
|
Usage: fmt.Sprintf("associate `ROLE` to the token (available: %v)", []user.Role{user.RoleReader, user.RoleWriter, user.RoleAdmin}),
|
||||||
Value: string(thirdparty.RoleReader),
|
Value: string(user.RoleReader),
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "subject",
|
Name: "subject",
|
||||||
Usage: "associate `SUBJECT` to the token",
|
Usage: "associate `SUBJECT` to the token",
|
||||||
Value: fmt.Sprintf("user-%s", shortuuid.New()),
|
Value: fmt.Sprintf("user-%s", uuid.New().String()),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "tenant",
|
||||||
|
Usage: "associate `TENANT` to the token",
|
||||||
|
Value: "00000000-0000-0000-0000-000000000000",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "output",
|
Name: "output",
|
||||||
|
@ -44,6 +50,7 @@ func CreateTokenCommand() *cli.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
subject := ctx.String("subject")
|
subject := ctx.String("subject")
|
||||||
|
tenant := ctx.String("tenant")
|
||||||
role := ctx.String("role")
|
role := ctx.String("role")
|
||||||
output := ctx.String("output")
|
output := ctx.String("output")
|
||||||
|
|
||||||
|
@ -57,7 +64,7 @@ func CreateTokenCommand() *cli.Command {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := thirdparty.GenerateToken(ctx.Context, key, subject, thirdparty.Role(role))
|
token, err := user.GenerateToken(ctx.Context, key, datastore.TenantID(tenant), subject, user.Role(role))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
|
@ -23,9 +23,10 @@ func NewDefaultServerConfig() ServerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
Local *LocalAuthConfig `yaml:"local"`
|
Local *LocalAuthConfig `yaml:"local"`
|
||||||
Remote *RemoteAuthConfig `yaml:"remote"`
|
Remote *RemoteAuthConfig `yaml:"remote"`
|
||||||
RoleExtractionRules []string `yaml:"roleExtractionRules"`
|
RoleExtractionRules []string `yaml:"roleExtractionRules"`
|
||||||
|
TenantExtractionRules []string `yaml:"tenantExtractionRules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultAuthConfig() AuthConfig {
|
func NewDefaultAuthConfig() AuthConfig {
|
||||||
|
@ -35,7 +36,10 @@ func NewDefaultAuthConfig() AuthConfig {
|
||||||
},
|
},
|
||||||
Remote: nil,
|
Remote: nil,
|
||||||
RoleExtractionRules: []string{
|
RoleExtractionRules: []string{
|
||||||
fmt.Sprintf("jwt.%s != nil ? str(jwt.%s) : ''", thirdparty.DefaultRoleKey, thirdparty.DefaultRoleKey),
|
fmt.Sprintf("jwt.%s != nil ? str(jwt.%s) : ''", user.DefaultRoleKey, user.DefaultRoleKey),
|
||||||
|
},
|
||||||
|
TenantExtractionRules: []string{
|
||||||
|
fmt.Sprintf("jwt.%s != nil ? str(jwt.%s) : ''", user.DefaultTenantKey, user.DefaultTenantKey),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ type Agent struct {
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
ContactedAt *time.Time `json:"contactedAt,omitempty"`
|
ContactedAt *time.Time `json:"contactedAt,omitempty"`
|
||||||
|
TenantID *TenantID `json:"tenantId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SerializableKeySet struct {
|
type SerializableKeySet struct {
|
||||||
|
|
|
@ -9,6 +9,10 @@ import (
|
||||||
|
|
||||||
type AgentRepository interface {
|
type AgentRepository interface {
|
||||||
Create(ctx context.Context, thumbprint string, keySet jwk.Set, metadata map[string]any) (*Agent, error)
|
Create(ctx context.Context, thumbprint string, keySet jwk.Set, metadata map[string]any) (*Agent, error)
|
||||||
|
|
||||||
|
Attach(ctx context.Context, tenantID TenantID, agentID AgentID) (*Agent, error)
|
||||||
|
Detach(ctx context.Context, agentID AgentID) (*Agent, error)
|
||||||
|
|
||||||
Get(ctx context.Context, id AgentID) (*Agent, error)
|
Get(ctx context.Context, id AgentID) (*Agent, error)
|
||||||
Update(ctx context.Context, id AgentID, updates ...AgentUpdateOptionFunc) (*Agent, error)
|
Update(ctx context.Context, id AgentID, updates ...AgentUpdateOptionFunc) (*Agent, error)
|
||||||
Query(ctx context.Context, opts ...AgentQueryOptionFunc) ([]*Agent, int, error)
|
Query(ctx context.Context, opts ...AgentQueryOptionFunc) ([]*Agent, int, error)
|
||||||
|
@ -25,6 +29,7 @@ type AgentQueryOptions struct {
|
||||||
Limit *int
|
Limit *int
|
||||||
Offset *int
|
Offset *int
|
||||||
IDs []AgentID
|
IDs []AgentID
|
||||||
|
TenantIDs []TenantID
|
||||||
Thumbprints []string
|
Thumbprints []string
|
||||||
Metadata *map[string]any
|
Metadata *map[string]any
|
||||||
Statuses []AgentStatus
|
Statuses []AgentStatus
|
||||||
|
@ -54,6 +59,12 @@ func WithAgentQueryID(ids ...AgentID) AgentQueryOptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithAgentQueryTenantID(ids ...TenantID) AgentQueryOptionFunc {
|
||||||
|
return func(opts *AgentQueryOptions) {
|
||||||
|
opts.TenantIDs = ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithAgentQueryStatus(statuses ...AgentStatus) AgentQueryOptionFunc {
|
func WithAgentQueryStatus(statuses ...AgentStatus) AgentQueryOptionFunc {
|
||||||
return func(opts *AgentQueryOptions) {
|
return func(opts *AgentQueryOptions) {
|
||||||
opts.Statuses = statuses
|
opts.Statuses = statuses
|
||||||
|
@ -75,6 +86,13 @@ type AgentUpdateOptions struct {
|
||||||
Metadata *map[string]any
|
Metadata *map[string]any
|
||||||
KeySet *jwk.Set
|
KeySet *jwk.Set
|
||||||
Thumbprint *string
|
Thumbprint *string
|
||||||
|
TenantID *TenantID
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAgentUpdateTenant(id TenantID) AgentUpdateOptionFunc {
|
||||||
|
return func(opts *AgentUpdateOptions) {
|
||||||
|
opts.TenantID = &id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
|
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
|
||||||
|
|
|
@ -6,4 +6,5 @@ var (
|
||||||
ErrNotFound = errors.New("not found")
|
ErrNotFound = errors.New("not found")
|
||||||
ErrAlreadyExist = errors.New("already exist")
|
ErrAlreadyExist = errors.New("already exist")
|
||||||
ErrUnexpectedRevision = errors.New("unexpected revision")
|
ErrUnexpectedRevision = errors.New("unexpected revision")
|
||||||
|
ErrAlreadyAttached = errors.New("already attached")
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,6 +15,8 @@ type Spec struct {
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
TenantID TenantID `json:"tenantId"`
|
||||||
|
AgentID AgentID `json:"agentId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecName() spec.Name {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
@ -16,8 +15,116 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentRepository struct {
|
type AgentRepository struct {
|
||||||
db *sql.DB
|
repository
|
||||||
sqliteBusyRetryMaxAttempts int
|
}
|
||||||
|
|
||||||
|
// Attach implements datastore.AgentRepository.
|
||||||
|
func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantID, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||||
|
var agent datastore.Agent
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
query := `SELECT count(id), tenant_id FROM agents WHERE id = $1`
|
||||||
|
row := tx.QueryRowContext(ctx, query, agentID)
|
||||||
|
|
||||||
|
var (
|
||||||
|
count int
|
||||||
|
attachedTenantID *datastore.TenantID
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := row.Scan(&count, &attachedTenantID); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attachedTenantID != nil {
|
||||||
|
return errors.WithStack(datastore.ErrAlreadyAttached)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
query = `
|
||||||
|
UPDATE agents SET tenant_id = $1, updated_at = $2 WHERE id = $3
|
||||||
|
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||||
|
`
|
||||||
|
|
||||||
|
row = tx.QueryRowContext(
|
||||||
|
ctx, query,
|
||||||
|
tenantID,
|
||||||
|
now,
|
||||||
|
agentID,
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata := JSONMap{}
|
||||||
|
var rawKeySet []byte
|
||||||
|
|
||||||
|
err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.Metadata = metadata
|
||||||
|
|
||||||
|
keySet, err := jwk.Parse(rawKeySet)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.KeySet = &datastore.SerializableKeySet{keySet}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &agent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detach implements datastore.AgentRepository.
|
||||||
|
func (r *AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||||
|
var agent datastore.Agent
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
UPDATE agents SET tenant_id = null, updated_at = $1 WHERE id = $2
|
||||||
|
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||||
|
`
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(
|
||||||
|
ctx, query,
|
||||||
|
now,
|
||||||
|
agentID,
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata := JSONMap{}
|
||||||
|
var rawKeySet []byte
|
||||||
|
|
||||||
|
err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.Metadata = metadata
|
||||||
|
|
||||||
|
keySet, err := jwk.Parse(rawKeySet)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.KeySet = &datastore.SerializableKeySet{keySet}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &agent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSpec implements datastore.AgentRepository.
|
// DeleteSpec implements datastore.AgentRepository.
|
||||||
|
@ -123,8 +230,8 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO specs (agent_id, name, revision, data, created_at, updated_at)
|
INSERT INTO specs (agent_id, name, revision, data, created_at, updated_at, tenant_id)
|
||||||
VALUES($1, $2, $3, $4, $5, $5)
|
VALUES($1, $2, $3, $4, $5, $5, ( SELECT tenant_id FROM agents WHERE id = $1 ))
|
||||||
ON CONFLICT (agent_id, name) DO UPDATE SET
|
ON CONFLICT (agent_id, name) DO UPDATE SET
|
||||||
data = $4, updated_at = $5, revision = specs.revision + 1
|
data = $4, updated_at = $5, revision = specs.revision + 1
|
||||||
WHERE revision = $3
|
WHERE revision = $3
|
||||||
|
@ -170,7 +277,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at FROM agents`
|
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at, tenant_id FROM agents`
|
||||||
|
|
||||||
limit := 10
|
limit := 10
|
||||||
if options.Limit != nil {
|
if options.Limit != nil {
|
||||||
|
@ -193,6 +300,13 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||||
args = append(args, newArgs...)
|
args = append(args, newArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.TenantIDs != nil && len(options.TenantIDs) > 0 {
|
||||||
|
filter, newArgs, newParamIndex := inFilter("tenant_id", paramIndex, options.TenantIDs)
|
||||||
|
filters += filter
|
||||||
|
paramIndex = newParamIndex
|
||||||
|
args = append(args, newArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
if options.Thumbprints != nil && len(options.Thumbprints) > 0 {
|
if options.Thumbprints != nil && len(options.Thumbprints) > 0 {
|
||||||
if filters != "" {
|
if filters != "" {
|
||||||
filters += " AND "
|
filters += " AND "
|
||||||
|
@ -240,7 +354,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||||
metadata := JSONMap{}
|
metadata := JSONMap{}
|
||||||
contactedAt := sql.NullTime{}
|
contactedAt := sql.NullTime{}
|
||||||
|
|
||||||
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +407,7 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet
|
||||||
query = `
|
query = `
|
||||||
INSERT INTO agents (thumbprint, keyset, metadata, status, created_at, updated_at)
|
INSERT INTO agents (thumbprint, keyset, metadata, status, created_at, updated_at)
|
||||||
VALUES($1, $2, $3, $4, $5, $5)
|
VALUES($1, $2, $3, $4, $5, $5)
|
||||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
|
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||||
`
|
`
|
||||||
|
|
||||||
rawKeySet, err := json.Marshal(keySet)
|
rawKeySet, err := json.Marshal(keySet)
|
||||||
|
@ -308,7 +422,7 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet
|
||||||
|
|
||||||
metadata := JSONMap{}
|
metadata := JSONMap{}
|
||||||
|
|
||||||
err = row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt)
|
err = row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -363,7 +477,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||||
|
|
||||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
query := `
|
query := `
|
||||||
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at", "tenant_id"
|
||||||
FROM agents
|
FROM agents
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
@ -374,7 +488,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||||
contactedAt := sql.NullTime{}
|
contactedAt := sql.NullTime{}
|
||||||
var rawKeySet []byte
|
var rawKeySet []byte
|
||||||
|
|
||||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return datastore.ErrNotFound
|
return datastore.ErrNotFound
|
||||||
}
|
}
|
||||||
|
@ -476,7 +590,7 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||||
|
|
||||||
query += `
|
query += `
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at", "tenant_id"
|
||||||
`
|
`
|
||||||
|
|
||||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||||
|
@ -487,7 +601,7 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||||
contactedAt := sql.NullTime{}
|
contactedAt := sql.NullTime{}
|
||||||
var rawKeySet []byte
|
var rawKeySet []byte
|
||||||
|
|
||||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return datastore.ErrNotFound
|
return datastore.ErrNotFound
|
||||||
}
|
}
|
||||||
|
@ -536,109 +650,8 @@ func (r *AgentRepository) agentExists(ctx context.Context, tx *sql.Tx, agentID d
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AgentRepository) withTxRetry(ctx context.Context, fn func(*sql.Tx) error) error {
|
|
||||||
attempts := 0
|
|
||||||
max := r.sqliteBusyRetryMaxAttempts
|
|
||||||
|
|
||||||
ctx = logger.With(ctx, logger.F("max", max))
|
|
||||||
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
ctx = logger.With(ctx)
|
|
||||||
|
|
||||||
if attempts >= max {
|
|
||||||
logger.Debug(ctx, "transaction retrying failed", logger.F("attempts", attempts))
|
|
||||||
|
|
||||||
return errors.Wrapf(err, "transaction failed after %d attempts", max)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.withTx(ctx, fn)
|
|
||||||
if err != nil {
|
|
||||||
if !strings.Contains(err.Error(), "(5) (SQLITE_BUSY)") {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
|
|
||||||
logger.Warn(ctx, "database is busy", logger.E(err))
|
|
||||||
|
|
||||||
wait := time.Duration(8<<(attempts+1)) * time.Millisecond
|
|
||||||
|
|
||||||
logger.Debug(
|
|
||||||
ctx, "database is busy, waiting before retrying transaction",
|
|
||||||
logger.F("wait", wait.String()),
|
|
||||||
logger.F("attempts", attempts),
|
|
||||||
)
|
|
||||||
|
|
||||||
timer := time.NewTimer(wait)
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
attempts++
|
|
||||||
continue
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *AgentRepository) withTx(ctx context.Context, fn func(*sql.Tx) error) error {
|
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := tx.Rollback(); err != nil {
|
|
||||||
if errors.Is(err, sql.ErrTxDone) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not rollback transaction", logger.CapturedE(err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := fn(tx); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAgentRepository(db *sql.DB, sqliteBusyRetryMaxAttempts int) *AgentRepository {
|
func NewAgentRepository(db *sql.DB, sqliteBusyRetryMaxAttempts int) *AgentRepository {
|
||||||
return &AgentRepository{db, sqliteBusyRetryMaxAttempts}
|
return &AgentRepository{repository{db, sqliteBusyRetryMaxAttempts}}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ datastore.AgentRepository = &AgentRepository{}
|
var _ datastore.AgentRepository = &AgentRepository{}
|
||||||
|
|
||||||
func inFilter[T any](column string, paramIndex int, items []T) (string, []any, int) {
|
|
||||||
args := make([]any, 0, len(items))
|
|
||||||
filter := fmt.Sprintf("%s in (", column)
|
|
||||||
|
|
||||||
for idx, item := range items {
|
|
||||||
if idx != 0 {
|
|
||||||
filter += ","
|
|
||||||
}
|
|
||||||
|
|
||||||
filter += fmt.Sprintf("$%d", paramIndex)
|
|
||||||
paramIndex++
|
|
||||||
|
|
||||||
args = append(args, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
filter += ")"
|
|
||||||
|
|
||||||
return filter, args, paramIndex
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
db *sql.DB
|
||||||
|
sqliteBusyRetryMaxAttempts int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) withTxRetry(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||||
|
attempts := 0
|
||||||
|
max := r.sqliteBusyRetryMaxAttempts
|
||||||
|
|
||||||
|
ctx = logger.With(ctx, logger.F("max", max))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
ctx = logger.With(ctx)
|
||||||
|
|
||||||
|
if attempts >= max {
|
||||||
|
logger.Debug(ctx, "transaction retrying failed", logger.F("attempts", attempts))
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "transaction failed after %d attempts", max)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.withTx(ctx, fn)
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "(5) (SQLITE_BUSY)") {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
logger.Warn(ctx, "database is busy", logger.E(err))
|
||||||
|
|
||||||
|
wait := time.Duration(8<<(attempts+1)) * time.Millisecond
|
||||||
|
|
||||||
|
logger.Debug(
|
||||||
|
ctx, "database is busy, waiting before retrying transaction",
|
||||||
|
logger.F("wait", wait.String()),
|
||||||
|
logger.F("attempts", attempts),
|
||||||
|
)
|
||||||
|
|
||||||
|
timer := time.NewTimer(wait)
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
attempts++
|
||||||
|
continue
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) withTx(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := tx.Rollback(); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrTxDone) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not rollback transaction", logger.CapturedE(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := fn(tx); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func inFilter[T any](column string, paramIndex int, items []T) (string, []any, int) {
|
||||||
|
args := make([]any, 0, len(items))
|
||||||
|
filter := fmt.Sprintf("%s in (", column)
|
||||||
|
|
||||||
|
for idx, item := range items {
|
||||||
|
if idx != 0 {
|
||||||
|
filter += ","
|
||||||
|
}
|
||||||
|
|
||||||
|
filter += fmt.Sprintf("$%d", paramIndex)
|
||||||
|
paramIndex++
|
||||||
|
|
||||||
|
args = append(args, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter += ")"
|
||||||
|
|
||||||
|
return filter, args, paramIndex
|
||||||
|
}
|
|
@ -0,0 +1,284 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TenantRepository struct {
|
||||||
|
repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query implements datastore.TenantRepository.
|
||||||
|
func (r *TenantRepository) Query(ctx context.Context, opts ...datastore.TenantQueryOptionFunc) ([]*datastore.Tenant, int, error) {
|
||||||
|
options := &datastore.TenantQueryOptions{}
|
||||||
|
for _, fn := range opts {
|
||||||
|
fn(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenants := make([]*datastore.Tenant, 0)
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
query := `SELECT id, label, created_at, updated_at FROM tenants`
|
||||||
|
|
||||||
|
limit := 10
|
||||||
|
if options.Limit != nil {
|
||||||
|
limit = *options.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
if options.Offset != nil {
|
||||||
|
offset = *options.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
filters := ""
|
||||||
|
paramIndex := 3
|
||||||
|
args := []any{offset, limit}
|
||||||
|
|
||||||
|
if options.IDs != nil && len(options.IDs) > 0 {
|
||||||
|
filter, newArgs, newParamIndex := inFilter("id", paramIndex, options.IDs)
|
||||||
|
filters += filter
|
||||||
|
paramIndex = newParamIndex
|
||||||
|
args = append(args, newArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters != "" {
|
||||||
|
filters = ` WHERE ` + filters
|
||||||
|
}
|
||||||
|
|
||||||
|
query += filters + ` LIMIT $2 OFFSET $1`
|
||||||
|
|
||||||
|
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||||
|
|
||||||
|
rows, err := tx.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not close rows", logger.CapturedE(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
tenant := &datastore.Tenant{}
|
||||||
|
|
||||||
|
if err := rows.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenants = append(tenants, tenant)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM tenants `+filters, args...)
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenants, count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements datastore.TenantRepository.
|
||||||
|
func (r *TenantRepository) Create(ctx context.Context, label string) (*datastore.Tenant, error) {
|
||||||
|
var tenant datastore.Tenant
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
INSERT INTO tenants (id, label, created_at, updated_at)
|
||||||
|
VALUES($1, $2, $3, $3)
|
||||||
|
RETURNING "id", "label", "created_at", "updated_at"
|
||||||
|
`
|
||||||
|
|
||||||
|
tenantID := datastore.NewTenantID()
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(
|
||||||
|
ctx, query,
|
||||||
|
tenantID, label, now,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := row.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tenant, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete implements datastore.TenantRepository.
|
||||||
|
func (r *TenantRepository) Delete(ctx context.Context, id datastore.TenantID) error {
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
if exists, err := r.tenantExists(ctx, tx, id); !exists {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `DELETE FROM tenants WHERE id = $1`
|
||||||
|
_, err := tx.ExecContext(ctx, query, id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = `DELETE FROM agents WHERE tenant_id = $1`
|
||||||
|
_, err = tx.ExecContext(ctx, query, id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = `DELETE FROM specs WHERE tenant_id = $1`
|
||||||
|
_, err = tx.ExecContext(ctx, query, id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements datastore.TenantRepository.
|
||||||
|
func (r *TenantRepository) Get(ctx context.Context, id datastore.TenantID) (*datastore.Tenant, error) {
|
||||||
|
var tenant datastore.Tenant
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
query := `
|
||||||
|
SELECT "id", "label", "created_at", "updated_at"
|
||||||
|
FROM tenants
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(ctx, query, id)
|
||||||
|
|
||||||
|
if err := row.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tenant, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update implements datastore.TenantRepository.
|
||||||
|
func (r *TenantRepository) Update(ctx context.Context, id datastore.TenantID, updates ...datastore.TenantUpdateOptionFunc) (*datastore.Tenant, error) {
|
||||||
|
options := &datastore.TenantUpdateOptions{}
|
||||||
|
for _, fn := range updates {
|
||||||
|
fn(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenant datastore.Tenant
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
if exists, err := r.tenantExists(ctx, tx, id); !exists {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
UPDATE tenants SET updated_at = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
args := []any{id}
|
||||||
|
index := 2
|
||||||
|
|
||||||
|
if options.Label != nil {
|
||||||
|
query += fmt.Sprintf(`, label = $%d`, index)
|
||||||
|
args = append(args, *options.Label)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := options.Label != nil
|
||||||
|
if updated {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
query += fmt.Sprintf(`, updated_at = $%d`, index)
|
||||||
|
args = append(args, now)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
query += `
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING "id", "label", "created_at", "updated_at"
|
||||||
|
`
|
||||||
|
|
||||||
|
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(ctx, query, args...)
|
||||||
|
|
||||||
|
if err := row.Scan(&tenant.ID, &tenant.Label, &tenant.CreatedAt, &tenant.UpdatedAt); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tenant, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TenantRepository) tenantExists(ctx context.Context, tx *sql.Tx, tenantID datastore.TenantID) (bool, error) {
|
||||||
|
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM tenants WHERE id = $1`, tenantID)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return false, errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return false, errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTenantRepository(db *sql.DB, sqliteBusyRetryMaxAttempts int) *TenantRepository {
|
||||||
|
return &TenantRepository{
|
||||||
|
repository: repository{db, sqliteBusyRetryMaxAttempts},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ datastore.TenantRepository = &TenantRepository{}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/testsuite"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/migrate"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSQLiteTeantRepository(t *testing.T) {
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
|
||||||
|
file := "testdata/tenant_repository_test.sqlite"
|
||||||
|
|
||||||
|
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||||
|
|
||||||
|
migr, err := migrate.New("../../../migrations", "sqlite", "sqlite://"+dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migr.Up(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite", dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := NewTenantRepository(db, 5)
|
||||||
|
|
||||||
|
testsuite.TestTenantRepository(t, repo)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultTenantID TenantID = "00000000-0000-0000-0000-000000000000"
|
||||||
|
|
||||||
|
type TenantID string
|
||||||
|
|
||||||
|
func NewTenantID() TenantID {
|
||||||
|
return TenantID(uuid.New().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTenantID(raw string) (TenantID, error) {
|
||||||
|
uuid, err := uuid.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TenantID(uuid.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tenant struct {
|
||||||
|
ID TenantID `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type TenantRepository interface {
|
||||||
|
Create(ctx context.Context, label string) (*Tenant, error)
|
||||||
|
Get(ctx context.Context, id TenantID) (*Tenant, error)
|
||||||
|
Update(ctx context.Context, id TenantID, updates ...TenantUpdateOptionFunc) (*Tenant, error)
|
||||||
|
Delete(ctx context.Context, id TenantID) error
|
||||||
|
|
||||||
|
Query(ctx context.Context, opts ...TenantQueryOptionFunc) ([]*Tenant, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TenantUpdateOptionFunc func(*TenantUpdateOptions)
|
||||||
|
|
||||||
|
type TenantUpdateOptions struct {
|
||||||
|
Label *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTenantUpdateLabel(label string) TenantUpdateOptionFunc {
|
||||||
|
return func(opts *TenantUpdateOptions) {
|
||||||
|
opts.Label = &label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TenantQueryOptionFunc func(*TenantQueryOptions)
|
||||||
|
|
||||||
|
type TenantQueryOptions struct {
|
||||||
|
Limit *int
|
||||||
|
Offset *int
|
||||||
|
IDs []TenantID
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTenantQueryLimit(limit int) TenantQueryOptionFunc {
|
||||||
|
return func(opts *TenantQueryOptions) {
|
||||||
|
opts.Limit = &limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTenantQueryOffset(offset int) TenantQueryOptionFunc {
|
||||||
|
return func(opts *TenantQueryOptions) {
|
||||||
|
opts.Offset = &offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTenantQueryID(ids ...TenantID) TenantQueryOptionFunc {
|
||||||
|
return func(opts *TenantQueryOptions) {
|
||||||
|
opts.IDs = ids
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package testsuite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTenantRepository(t *testing.T, repo datastore.TenantRepository) {
|
||||||
|
t.Run("Cases", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
runTenantRepositoryTests(t, repo)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package testsuite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tenantRepositoryTestCase struct {
|
||||||
|
Name string
|
||||||
|
Skip bool
|
||||||
|
Run func(ctx context.Context, repo datastore.TenantRepository) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantRepositoryTestCases = []tenantRepositoryTestCase{
|
||||||
|
{
|
||||||
|
Name: "Create a new tenant",
|
||||||
|
Run: func(ctx context.Context, repo datastore.TenantRepository) error {
|
||||||
|
label := "Foo"
|
||||||
|
tenant, err := repo.Create(ctx, "Foo")
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tenant.CreatedAt.IsZero() {
|
||||||
|
return errors.Errorf("tenant.CreatedAt should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tenant.UpdatedAt.IsZero() {
|
||||||
|
return errors.Errorf("tenant.UpdatedAt should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := label, tenant.Label; e != g {
|
||||||
|
return errors.Errorf("tenant.Label: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tenant.ID == "" {
|
||||||
|
return errors.Errorf("tenant.ID should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := datastore.ParseTenantID(string(tenant.ID)); err != nil {
|
||||||
|
return errors.Wrapf(err, "tenant.ID should be valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Try to update an unexistant tenant",
|
||||||
|
Run: func(ctx context.Context, repo datastore.TenantRepository) error {
|
||||||
|
unexistantTenantID := datastore.TenantID("00000000-0000-0000-0000-000000000000")
|
||||||
|
tenant, err := repo.Update(ctx, unexistantTenantID)
|
||||||
|
if err == nil {
|
||||||
|
return errors.New("error should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tenant != nil {
|
||||||
|
return errors.New("tenant should be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Try to delete spec of an unexistant agent",
|
||||||
|
Run: func(ctx context.Context, repo datastore.TenantRepository) error {
|
||||||
|
unexistantTenantID := datastore.TenantID("00000000-0000-0000-0000-000000000000")
|
||||||
|
|
||||||
|
err := repo.Delete(ctx, unexistantTenantID)
|
||||||
|
if err == nil {
|
||||||
|
return errors.New("error should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTenantRepositoryTests(t *testing.T, repo datastore.TenantRepository) {
|
||||||
|
for _, tc := range tenantRepositoryTestCases {
|
||||||
|
func(tc tenantRepositoryTestCase) {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if tc.Skip {
|
||||||
|
t.SkipNow()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := tc.Run(ctx, repo); err != nil {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}(tc)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Format format.Format = "json"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
format.Register(Format, NewWriter())
|
|
||||||
}
|
|
||||||
|
|
||||||
type Writer struct{}
|
|
||||||
|
|
||||||
// Format implements format.Writer.
|
|
||||||
func (*Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
|
|
||||||
encoder := json.NewEncoder(writer)
|
|
||||||
|
|
||||||
if hints.OutputMode == format.OutputModeWide {
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWriter() *Writer {
|
|
||||||
return &Writer{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ format.Writer = &Writer{}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package format
|
|
||||||
|
|
||||||
type Prop struct {
|
|
||||||
name string
|
|
||||||
label string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Prop) Name() string {
|
|
||||||
return p.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Prop) Label() string {
|
|
||||||
return p.label
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProp(name, label string) Prop {
|
|
||||||
return Prop{name, label}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package format
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Format string
|
|
||||||
|
|
||||||
type Registry map[Format]Writer
|
|
||||||
|
|
||||||
var defaultRegistry = Registry{}
|
|
||||||
|
|
||||||
var ErrUnknownFormat = errors.New("unknown format")
|
|
||||||
|
|
||||||
func Write(format Format, writer io.Writer, hints Hints, data ...any) error {
|
|
||||||
formatWriter, exists := defaultRegistry[format]
|
|
||||||
if !exists {
|
|
||||||
return errors.WithStack(ErrUnknownFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hints.OutputMode == "" {
|
|
||||||
hints.OutputMode = OutputModeCompact
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := formatWriter.Write(writer, hints, data...); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Available() []Format {
|
|
||||||
formats := make([]Format, 0, len(defaultRegistry))
|
|
||||||
|
|
||||||
for f := range defaultRegistry {
|
|
||||||
formats = append(formats, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return formats
|
|
||||||
}
|
|
||||||
|
|
||||||
func Register(format Format, writer Writer) {
|
|
||||||
defaultRegistry[format] = writer
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package table
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getProps(d any) []format.Prop {
|
|
||||||
props := make([]format.Prop, 0)
|
|
||||||
|
|
||||||
v := reflect.Indirect(reflect.ValueOf(d))
|
|
||||||
typeOf := v.Type()
|
|
||||||
|
|
||||||
for i := 0; i < v.NumField(); i++ {
|
|
||||||
name := typeOf.Field(i).Name
|
|
||||||
props = append(props, format.NewProp(name, name))
|
|
||||||
}
|
|
||||||
|
|
||||||
return props
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFieldValue(obj any, name string) string {
|
|
||||||
v := reflect.Indirect(reflect.ValueOf(obj))
|
|
||||||
|
|
||||||
fieldValue := v.FieldByName(name)
|
|
||||||
|
|
||||||
switch fieldValue.Kind() {
|
|
||||||
case reflect.Map:
|
|
||||||
fallthrough
|
|
||||||
case reflect.Struct:
|
|
||||||
fallthrough
|
|
||||||
case reflect.Slice:
|
|
||||||
fallthrough
|
|
||||||
case reflect.Interface:
|
|
||||||
json, err := json.Marshal(fieldValue.Interface())
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(json)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%v", fieldValue.Interface())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
package table
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"github.com/jedib0t/go-pretty/v6/table"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Format format.Format = "table"
|
|
||||||
|
|
||||||
const DefaultCompactModeMaxColumnWidth = 30
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
format.Register(Format, NewWriter(DefaultCompactModeMaxColumnWidth))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Writer struct {
|
|
||||||
compactModeMaxColumnWidth int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements format.Writer.
|
|
||||||
func (w *Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
|
|
||||||
t := table.NewWriter()
|
|
||||||
|
|
||||||
t.SetOutputMirror(writer)
|
|
||||||
|
|
||||||
var props []format.Prop
|
|
||||||
|
|
||||||
if hints.Props != nil {
|
|
||||||
props = hints.Props
|
|
||||||
} else {
|
|
||||||
if len(data) > 0 {
|
|
||||||
props = getProps(data[0])
|
|
||||||
} else {
|
|
||||||
props = make([]format.Prop, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labels := table.Row{}
|
|
||||||
|
|
||||||
for _, p := range props {
|
|
||||||
labels = append(labels, p.Label())
|
|
||||||
}
|
|
||||||
|
|
||||||
t.AppendHeader(labels)
|
|
||||||
|
|
||||||
isCompactMode := hints.OutputMode == format.OutputModeCompact
|
|
||||||
|
|
||||||
for _, d := range data {
|
|
||||||
row := table.Row{}
|
|
||||||
|
|
||||||
for _, p := range props {
|
|
||||||
value := getFieldValue(d, p.Name())
|
|
||||||
|
|
||||||
if isCompactMode && len(value) > w.compactModeMaxColumnWidth {
|
|
||||||
value = value[:w.compactModeMaxColumnWidth] + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
row = append(row, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.AppendRow(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Render()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWriter(compactModeMaxColumnWidth int) *Writer {
|
|
||||||
return &Writer{compactModeMaxColumnWidth}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ format.Writer = &Writer{}
|
|
|
@ -1,86 +0,0 @@
|
||||||
package table
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dummyItem struct {
|
|
||||||
MyString string
|
|
||||||
MyInt int
|
|
||||||
MySub subItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type subItem struct {
|
|
||||||
MyBool bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var dummyItems = []any{
|
|
||||||
dummyItem{
|
|
||||||
MyString: "Foo",
|
|
||||||
MyInt: 1,
|
|
||||||
MySub: subItem{
|
|
||||||
MyBool: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dummyItem{
|
|
||||||
MyString: "Bar",
|
|
||||||
MyInt: 0,
|
|
||||||
MySub: subItem{
|
|
||||||
MyBool: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriterNoHints(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
|
|
||||||
|
|
||||||
if err := writer.Write(&buf, format.Hints{}, dummyItems...); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := `+----------+-------+------------------+
|
|
||||||
| MYSTRING | MYINT | MYSUB |
|
|
||||||
+----------+-------+------------------+
|
|
||||||
| Foo | 1 | {"MyBool":false} |
|
|
||||||
| Bar | 0 | {"MyBool":true} |
|
|
||||||
+----------+-------+------------------+`
|
|
||||||
|
|
||||||
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
|
|
||||||
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriterWithPropHints(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
|
|
||||||
|
|
||||||
hints := format.Hints{
|
|
||||||
Props: []format.Prop{
|
|
||||||
format.NewProp("MyString", "MyString"),
|
|
||||||
format.NewProp("MyInt", "MyInt"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writer.Write(&buf, hints, dummyItems...); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := `+----------+-------+
|
|
||||||
| MYSTRING | MYINT |
|
|
||||||
+----------+-------+
|
|
||||||
| Foo | 1 |
|
|
||||||
| Bar | 0 |
|
|
||||||
+----------+-------+`
|
|
||||||
|
|
||||||
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
|
|
||||||
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package format
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type OutputMode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
OutputModeWide OutputMode = "wide"
|
|
||||||
OutputModeCompact OutputMode = "compact"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Hints struct {
|
|
||||||
Props []Prop
|
|
||||||
OutputMode OutputMode
|
|
||||||
}
|
|
||||||
|
|
||||||
type Writer interface {
|
|
||||||
Write(writer io.Writer, hints Hints, data ...any) error
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
package format
|
package format
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "forge.cadoles.com/Cadoles/emissary/internal/format/json"
|
_ "gitlab.com/wpetit/goweb/cli/format/json"
|
||||||
_ "forge.cadoles.com/Cadoles/emissary/internal/format/table"
|
_ "gitlab.com/wpetit/goweb/cli/format/table"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,420 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/api"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
|
||||||
ErrCodeNotFound api.ErrorCode = "not-found"
|
|
||||||
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
|
||||||
ErrCodeConflict api.ErrorCode = "conflict"
|
|
||||||
)
|
|
||||||
|
|
||||||
type registerAgentRequest struct {
|
|
||||||
KeySet json.RawMessage `json:"keySet" validate:"required"`
|
|
||||||
Metadata []metadata.Tuple `json:"metadata" validate:"required"`
|
|
||||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
|
||||||
Signature string `json:"signature" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
registerAgentReq := ®isterAgentRequest{}
|
|
||||||
if ok := api.Bind(w, r, registerAgentReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
keySet, err := jwk.Parse(registerAgentReq.KeySet)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not parse key set", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint))
|
|
||||||
|
|
||||||
// Validate that the existing signature validates the request
|
|
||||||
|
|
||||||
validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not validate signature", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validSignature {
|
|
||||||
logger.Warn(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature))
|
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata := metadata.FromSorted(registerAgentReq.Metadata)
|
|
||||||
|
|
||||||
agent, err := s.agentRepo.Create(
|
|
||||||
ctx,
|
|
||||||
registerAgentReq.Thumbprint,
|
|
||||||
keySet,
|
|
||||||
metadata,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, datastore.ErrAlreadyExist) {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not create agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agents, _, err := s.agentRepo.Query(
|
|
||||||
ctx,
|
|
||||||
datastore.WithAgentQueryThumbprints(registerAgentReq.Thumbprint),
|
|
||||||
datastore.WithAgentQueryLimit(1),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not retrieve agents", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(agents) == 0 {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not retrieve matching agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agentID := agents[0].ID
|
|
||||||
|
|
||||||
agent, err = s.agentRepo.Get(ctx, agentID)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(
|
|
||||||
ctx, "could not retrieve agent",
|
|
||||||
logger.CapturedE(err), logger.F("agentID", agentID),
|
|
||||||
)
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not validate signature using previous keyset", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agent, err = s.agentRepo.Update(
|
|
||||||
ctx, agents[0].ID,
|
|
||||||
datastore.WithAgentUpdateKeySet(keySet),
|
|
||||||
datastore.WithAgentUpdateMetadata(metadata),
|
|
||||||
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusCreated, struct {
|
|
||||||
Agent *datastore.Agent `json:"agent"`
|
|
||||||
}{
|
|
||||||
Agent: agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateAgentRequest struct {
|
|
||||||
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
|
||||||
Label *string `json:"label" validate:"omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
updateAgentReq := &updateAgentRequest{}
|
|
||||||
if ok := api.Bind(w, r, updateAgentReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options := make([]datastore.AgentUpdateOptionFunc, 0)
|
|
||||||
|
|
||||||
if updateAgentReq.Status != nil {
|
|
||||||
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
|
||||||
}
|
|
||||||
|
|
||||||
if updateAgentReq.Label != nil {
|
|
||||||
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
|
||||||
}
|
|
||||||
|
|
||||||
agent, err := s.agentRepo.Update(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
options...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Agent *datastore.Agent `json:"agent"`
|
|
||||||
}{
|
|
||||||
Agent: agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) queryAgents(w http.ResponseWriter, r *http.Request) {
|
|
||||||
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
offset, ok := getIntQueryParam(w, r, "offset", 0)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options := []datastore.AgentQueryOptionFunc{
|
|
||||||
datastore.WithAgentQueryLimit(int(limit)),
|
|
||||||
datastore.WithAgentQueryOffset(int(offset)),
|
|
||||||
}
|
|
||||||
|
|
||||||
ids, ok := getIntSliceValues(w, r, "ids", nil)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ids != nil {
|
|
||||||
agentIDs := func(ids []int64) []datastore.AgentID {
|
|
||||||
agentIDs := make([]datastore.AgentID, 0, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
agentIDs = append(agentIDs, datastore.AgentID(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
return agentIDs
|
|
||||||
}(ids)
|
|
||||||
|
|
||||||
options = append(options, datastore.WithAgentQueryID(agentIDs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbprints, ok := getStringSliceValues(w, r, "thumbprints", nil)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if thumbprints != nil {
|
|
||||||
options = append(options, datastore.WithAgentQueryThumbprints(thumbprints...))
|
|
||||||
}
|
|
||||||
|
|
||||||
statuses, ok := getIntSliceValues(w, r, "statuses", nil)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if statuses != nil {
|
|
||||||
agentStatuses := func(statuses []int64) []datastore.AgentStatus {
|
|
||||||
agentStatuses := make([]datastore.AgentStatus, 0, len(statuses))
|
|
||||||
for _, status := range statuses {
|
|
||||||
agentStatuses = append(agentStatuses, datastore.AgentStatus(status))
|
|
||||||
}
|
|
||||||
|
|
||||||
return agentStatuses
|
|
||||||
}(statuses)
|
|
||||||
|
|
||||||
options = append(options, datastore.WithAgentQueryStatus(agentStatuses...))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
agents, total, err := s.agentRepo.Query(
|
|
||||||
ctx,
|
|
||||||
options...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not list agents", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Agents []*datastore.Agent `json:"agents"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
}{
|
|
||||||
Agents: agents,
|
|
||||||
Total: total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
err := s.agentRepo.Delete(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not delete agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
AgentID datastore.AgentID `json:"agentId"`
|
|
||||||
}{
|
|
||||||
AgentID: datastore.AgentID(agentID),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
agent, err := s.agentRepo.Get(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Agent *datastore.Agent `json:"agent"`
|
|
||||||
}{
|
|
||||||
Agent: agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool) {
|
|
||||||
rawAgentID := chi.URLParam(r, "agentID")
|
|
||||||
|
|
||||||
agentID, err := strconv.ParseInt(rawAgentID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(r.Context(), "could not parse agent id", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return datastore.AgentID(agentID), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
|
||||||
rawValue := r.URL.Query().Get(param)
|
|
||||||
if rawValue != "" {
|
|
||||||
value, err := strconv.ParseInt(rawValue, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
|
|
||||||
rawValue := r.URL.Query().Get(param)
|
|
||||||
if rawValue != "" {
|
|
||||||
values := strings.Split(rawValue, ",")
|
|
||||||
|
|
||||||
return values, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIntSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []int64) ([]int64, bool) {
|
|
||||||
rawValue := r.URL.Query().Get(param)
|
|
||||||
|
|
||||||
if rawValue != "" {
|
|
||||||
rawValues := strings.Split(rawValue, ",")
|
|
||||||
values := make([]int64, 0, len(rawValues))
|
|
||||||
|
|
||||||
for _, rv := range rawValues {
|
|
||||||
value, err := strconv.ParseInt(rv, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(r.Context(), "could not parse int slice param", logger.F("param", param), logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return values, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue, true
|
|
||||||
}
|
|
|
@ -0,0 +1,251 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrCodeForbidden api.ErrorCode = "forbidden"
|
||||||
|
|
||||||
|
func assertQueryAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfRoles(user.RoleReader, user.RoleWriter, user.RoleAdmin),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertUserWithWriteAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfRoles(user.RoleWriter, user.RoleAdmin),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAgentOrUserWithWriteAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfRoles(user.RoleWriter, user.RoleAdmin),
|
||||||
|
assertMatchingAgent(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAgentOrUserWithReadAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfRoles(user.RoleReader, user.RoleWriter, user.RoleAdmin),
|
||||||
|
assertMatchingAgent(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAdminAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfRoles(user.RoleAdmin),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAdminOrTenantReadAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfUser(
|
||||||
|
assertOneOfRoles(user.RoleAdmin),
|
||||||
|
assertAllOfUser(
|
||||||
|
assertOneOfRoles(user.RoleReader, user.RoleWriter),
|
||||||
|
assertSameTenant(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAdminOrTenantWriteAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfUser(
|
||||||
|
assertOneOfRoles(user.RoleAdmin),
|
||||||
|
assertAllOfUser(
|
||||||
|
assertOneOfRoles(user.RoleWriter),
|
||||||
|
assertSameTenant(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAuthz(h http.Handler, assertUser assertUser, assertAgent assertAgent) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqUser, ok := assertRequestUser(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u := reqUser.(type) {
|
||||||
|
case *user.User:
|
||||||
|
if assertUser != nil {
|
||||||
|
if ok := assertUser(w, r, u); ok {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *agent.User:
|
||||||
|
if assertAgent != nil {
|
||||||
|
if ok := assertAgent(w, r, u); ok {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logUnexpectedUserType(r.Context(), reqUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidden(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type assertUser func(w http.ResponseWriter, r *http.Request, u *user.User) bool
|
||||||
|
type assertAgent func(w http.ResponseWriter, r *http.Request, u *agent.User) bool
|
||||||
|
|
||||||
|
func assertAllOfUser(funcs ...assertUser) assertUser {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, u *user.User) bool {
|
||||||
|
for _, fn := range funcs {
|
||||||
|
if ok := fn(w, r, u); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertOneOfUser(funcs ...assertUser) assertUser {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, u *user.User) bool {
|
||||||
|
for _, fn := range funcs {
|
||||||
|
if ok := fn(w, r, u); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSameTenant() assertUser {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, u *user.User) bool {
|
||||||
|
tenantID, ok := getTenantID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Tenant() == tenantID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertOneOfRoles(roles ...user.Role) assertUser {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, u *user.User) bool {
|
||||||
|
role := u.Role()
|
||||||
|
for _, rr := range roles {
|
||||||
|
if rr == role {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertMatchingAgent() assertAgent {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, u *agent.User) bool {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := u.Agent()
|
||||||
|
if agent != nil && agent.ID == agentID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool) {
|
||||||
|
ctx := r.Context()
|
||||||
|
user, err := auth.CtxUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not retrieve user", logger.CapturedE(err))
|
||||||
|
|
||||||
|
forbidden(w, r)
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil || user.Tenant() == "" {
|
||||||
|
forbidden(w, r)
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentID datastore.AgentID) bool {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
user, ok := assertRequestUser(w, r)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Get(ctx, agentID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if agent.TenantID != nil && *agent.TenantID == user.Tenant() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger.Warn(r.Context(), "forbidden", logger.F("path", r.URL.Path))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logUnexpectedUserType(ctx context.Context, user auth.User) {
|
||||||
|
logger.Warn(
|
||||||
|
ctx, "unexpected user type",
|
||||||
|
logger.F("subject", user.Subject()),
|
||||||
|
logger.F("type", fmt.Sprintf("%T", user)),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type claimAgentRequest struct {
|
||||||
|
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) claimAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, ok := assertRequestUser(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
claimAgentReq := &claimAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, claimAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results, _, err := m.agentRepo.Query(
|
||||||
|
ctx,
|
||||||
|
datastore.WithAgentQueryThumbprints(claimAgentReq.Thumbprint),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not query agents", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 1 {
|
||||||
|
logger.Error(ctx, "multiple results for agent thumbprint", logger.F("agentThumbprint", claimAgentReq.Thumbprint))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeMultipleResults, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := results[0]
|
||||||
|
|
||||||
|
if agent.TenantID != nil {
|
||||||
|
logger.Error(ctx, "agent already claimed", logger.F("agentThumbprint", claimAgentReq.Thumbprint), logger.F("agentID", agent.ID), logger.F("tenantID", agent.TenantID))
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyClaimed, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Attach(ctx, user.Tenant(), agent.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not attach agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateTenantRequest struct {
|
||||||
|
Label string `json:"label" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) createTenant(w http.ResponseWriter, r *http.Request) {
|
||||||
|
createTenantReq := &CreateTenantRequest{}
|
||||||
|
if ok := api.Bind(w, r, createTenantReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
tenant, err := m.tenantRepo.Create(ctx, createTenantReq.Label)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not create tenant", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Tenant *datastore.Tenant `json:"tenant"`
|
||||||
|
}{
|
||||||
|
Tenant: tenant,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
err := m.agentRepo.Delete(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not delete agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
AgentID datastore.AgentID `json:"agentId"`
|
||||||
|
}{
|
||||||
|
AgentID: datastore.AgentID(agentID),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) deleteTenant(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tenantID, ok := getTenantID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
err := m.tenantRepo.Delete(
|
||||||
|
ctx,
|
||||||
|
tenantID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not delete tenant", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
TenantID datastore.TenantID `json:"tenantId"`
|
||||||
|
}{
|
||||||
|
TenantID: tenantID,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) getAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Get(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
specs, err := m.agentRepo.GetSpecs(ctx, agentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not list specs", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Specs []*datastore.Spec `json:"specs"`
|
||||||
|
}{
|
||||||
|
Specs: specs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteSpecRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSpecReq := &deleteSpecRequest{}
|
||||||
|
if ok := api.Bind(w, r, deleteSpecReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
err := m.agentRepo.DeleteSpec(
|
||||||
|
ctx,
|
||||||
|
agentID,
|
||||||
|
deleteSpecReq.Name,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not delete spec", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{
|
||||||
|
Name: deleteSpecReq.Name,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) getTenant(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tenantID, ok := getTenantID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
tenant, err := m.tenantRepo.Get(ctx, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not get tenant", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Tenant *datastore.Tenant `json:"tenant"`
|
||||||
|
}{
|
||||||
|
Tenant: tenant,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
||||||
|
ErrCodeNotFound api.ErrorCode = "not-found"
|
||||||
|
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
||||||
|
ErrCodeConflict api.ErrorCode = "conflict"
|
||||||
|
ErrCodeMultipleResults api.ErrorCode = "multiple-results"
|
||||||
|
ErrCodeAlreadyClaimed api.ErrorCode = "already-claimed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool) {
|
||||||
|
rawAgentID := chi.URLParam(r, "agentID")
|
||||||
|
|
||||||
|
agentID, err := strconv.ParseInt(rawAgentID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(r.Context(), "could not parse agent id", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return datastore.AgentID(agentID), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
||||||
|
rawSpecID := chi.URLParam(r, "specID")
|
||||||
|
|
||||||
|
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(r.Context(), "could not parse spec id", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return datastore.SpecID(specID), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTenantID(w http.ResponseWriter, r *http.Request) (datastore.TenantID, bool) {
|
||||||
|
rawTenantID := chi.URLParam(r, "tenantID")
|
||||||
|
|
||||||
|
tenantID, err := datastore.ParseTenantID(rawTenantID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(r.Context(), "could not parse tenant id", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenantID, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
||||||
|
rawValue := r.URL.Query().Get(param)
|
||||||
|
if rawValue != "" {
|
||||||
|
value, err := strconv.ParseInt(rawValue, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
|
||||||
|
rawValue := r.URL.Query().Get(param)
|
||||||
|
if rawValue != "" {
|
||||||
|
values := strings.Split(rawValue, ",")
|
||||||
|
|
||||||
|
return values, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIntSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []int64) ([]int64, bool) {
|
||||||
|
rawValue := r.URL.Query().Get(param)
|
||||||
|
|
||||||
|
if rawValue != "" {
|
||||||
|
rawValues := strings.Split(rawValue, ",")
|
||||||
|
values := make([]int64, 0, len(rawValues))
|
||||||
|
|
||||||
|
for _, rv := range rawValues {
|
||||||
|
value, err := strconv.ParseInt(rv, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(r.Context(), "could not parse int slice param", logger.F("param", param), logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, true
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mount struct {
|
||||||
|
agentRepo datastore.AgentRepository
|
||||||
|
tenantRepo datastore.TenantRepository
|
||||||
|
authenticators []auth.Authenticator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) Mount(r chi.Router) {
|
||||||
|
r.NotFound(m.notFound)
|
||||||
|
|
||||||
|
r.Post("/register", m.registerAgent)
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(auth.Middleware(m.authenticators...))
|
||||||
|
|
||||||
|
r.Route("/agents", func(r chi.Router) {
|
||||||
|
r.With(assertUserWithWriteAccess).Post("/claim", m.claimAgent)
|
||||||
|
|
||||||
|
r.With(assertQueryAccess).Get("/", m.queryAgents)
|
||||||
|
|
||||||
|
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}", m.getAgent)
|
||||||
|
r.With(assertAgentOrUserWithWriteAccess).Put("/{agentID}", m.updateAgent)
|
||||||
|
r.With(assertUserWithWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
||||||
|
|
||||||
|
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
||||||
|
r.With(assertUserWithWriteAccess).Post("/{agentID}/specs", m.updateSpec)
|
||||||
|
r.With(assertUserWithWriteAccess).Delete("/{agentID}/specs", m.deleteSpec)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/tenants", func(r chi.Router) {
|
||||||
|
r.With(assertQueryAccess).Get("/", m.queryTenants)
|
||||||
|
r.With(assertAdminAccess).Post("/", m.createTenant)
|
||||||
|
r.With(assertAdminOrTenantReadAccess).Get("/{tenantID}", m.getTenant)
|
||||||
|
r.With(assertAdminOrTenantWriteAccess).Put("/{tenantID}", m.updateTenant)
|
||||||
|
r.With(assertAdminAccess).Delete("/{tenantID}", m.deleteTenant)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) notFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMount(agentRepo datastore.AgentRepository, tenantRepo datastore.TenantRepository, authenticators ...auth.Authenticator) *Mount {
|
||||||
|
return &Mount{agentRepo, tenantRepo, authenticators}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
userAuth "forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) queryAgents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
baseUser, ok := assertRequestUser(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
user, ok := baseUser.(*userAuth.User)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "unexpected user type", logger.F("user", baseUser))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, ok := getIntQueryParam(w, r, "offset", 0)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []datastore.AgentQueryOptionFunc{
|
||||||
|
datastore.WithAgentQueryLimit(int(limit)),
|
||||||
|
datastore.WithAgentQueryOffset(int(offset)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Role() != userAuth.RoleAdmin {
|
||||||
|
options = append(options, datastore.WithAgentQueryTenantID(user.Tenant()))
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, ok := getIntSliceValues(w, r, "ids", nil)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ids != nil {
|
||||||
|
agentIDs := func(ids []int64) []datastore.AgentID {
|
||||||
|
agentIDs := make([]datastore.AgentID, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
agentIDs = append(agentIDs, datastore.AgentID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return agentIDs
|
||||||
|
}(ids)
|
||||||
|
|
||||||
|
options = append(options, datastore.WithAgentQueryID(agentIDs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbprints, ok := getStringSliceValues(w, r, "thumbprints", nil)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if thumbprints != nil {
|
||||||
|
options = append(options, datastore.WithAgentQueryThumbprints(thumbprints...))
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses, ok := getIntSliceValues(w, r, "statuses", nil)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if statuses != nil {
|
||||||
|
agentStatuses := func(statuses []int64) []datastore.AgentStatus {
|
||||||
|
agentStatuses := make([]datastore.AgentStatus, 0, len(statuses))
|
||||||
|
for _, status := range statuses {
|
||||||
|
agentStatuses = append(agentStatuses, datastore.AgentStatus(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
return agentStatuses
|
||||||
|
}(statuses)
|
||||||
|
|
||||||
|
options = append(options, datastore.WithAgentQueryStatus(agentStatuses...))
|
||||||
|
}
|
||||||
|
|
||||||
|
agents, total, err := m.agentRepo.Query(
|
||||||
|
ctx,
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not list agents", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agents []*datastore.Agent `json:"agents"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}{
|
||||||
|
Agents: agents,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
userAuth "forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) queryTenants(w http.ResponseWriter, r *http.Request) {
|
||||||
|
baseUser, ok := assertRequestUser(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
user, ok := baseUser.(*userAuth.User)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "unexpected user type", logger.F("user", baseUser))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, ok := getIntQueryParam(w, r, "offset", 0)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []datastore.TenantQueryOptionFunc{
|
||||||
|
datastore.WithTenantQueryLimit(int(limit)),
|
||||||
|
datastore.WithTenantQueryOffset(int(offset)),
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, ok := getStringSliceValues(w, r, "ids", nil)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantIDs := make([]datastore.TenantID, 0)
|
||||||
|
|
||||||
|
if user.Role() != userAuth.RoleAdmin {
|
||||||
|
tenantIDs = append(tenantIDs, user.Tenant())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
tenantIDs = append(tenantIDs, datastore.TenantID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tenantIDs) > 0 {
|
||||||
|
options = append(options, datastore.WithTenantQueryID(tenantIDs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenants, total, err := m.tenantRepo.Query(
|
||||||
|
ctx,
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not list tenants", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Tenants []*datastore.Tenant `json:"tenants"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}{
|
||||||
|
Tenants: tenants,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type registerAgentRequest struct {
|
||||||
|
KeySet json.RawMessage `json:"keySet" validate:"required"`
|
||||||
|
Metadata []metadata.Tuple `json:"metadata" validate:"required"`
|
||||||
|
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||||
|
Signature string `json:"signature" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
registerAgentReq := ®isterAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, registerAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
keySet, err := jwk.Parse(registerAgentReq.KeySet)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not parse key set", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint))
|
||||||
|
|
||||||
|
// Validate that the existing signature validates the request
|
||||||
|
|
||||||
|
validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not validate signature", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validSignature {
|
||||||
|
logger.Warn(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature))
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := metadata.FromSorted(registerAgentReq.Metadata)
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Create(
|
||||||
|
ctx,
|
||||||
|
registerAgentReq.Thumbprint,
|
||||||
|
keySet,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, datastore.ErrAlreadyExist) {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not create agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agents, _, err := m.agentRepo.Query(
|
||||||
|
ctx,
|
||||||
|
datastore.WithAgentQueryThumbprints(registerAgentReq.Thumbprint),
|
||||||
|
datastore.WithAgentQueryLimit(1),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not retrieve agents", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(agents) == 0 {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not retrieve matching agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agentID := agents[0].ID
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Get(ctx, agentID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not retrieve agent",
|
||||||
|
logger.CapturedE(err), logger.F("agentID", agentID),
|
||||||
|
)
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not validate signature using previous keyset", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validSignature {
|
||||||
|
logger.Error(ctx, "invalid signature")
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Update(
|
||||||
|
ctx,
|
||||||
|
agents[0].ID,
|
||||||
|
datastore.WithAgentUpdateKeySet(keySet),
|
||||||
|
datastore.WithAgentUpdateMetadata(metadata),
|
||||||
|
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusCreated, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type releaseAgentRequest struct {
|
||||||
|
AgentID int64 `json:"agentId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) releaseAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
releaseAgentReq := &releaseAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, releaseAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agentID := datastore.AgentID(releaseAgentReq.AgentID)
|
||||||
|
|
||||||
|
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Get(ctx, agentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not retrieve agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Detach(ctx, agent.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not detach agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateAgentRequest struct {
|
||||||
|
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
||||||
|
Label *string `json:"label" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
updateAgentReq := &updateAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, updateAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]datastore.AgentUpdateOptionFunc, 0)
|
||||||
|
|
||||||
|
if updateAgentReq.Status != nil {
|
||||||
|
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateAgentReq.Label != nil {
|
||||||
|
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Update(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateSpecRequest struct {
|
||||||
|
spec.RawSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
updateSpecReq := &updateSpecRequest{}
|
||||||
|
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := spec.Validate(ctx, updateSpecReq); err != nil {
|
||||||
|
data := struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
var validationErr *spec.ValidationError
|
||||||
|
|
||||||
|
if errors.As(err, &validationErr) {
|
||||||
|
data.Message = validationErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not validate spec", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, err := m.agentRepo.UpdateSpec(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
string(updateSpecReq.SpecName()),
|
||||||
|
updateSpecReq.SpecRevision(),
|
||||||
|
updateSpecReq.SpecData(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, datastore.ErrUnexpectedRevision) {
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeUnexpectedRevision, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not update spec", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Spec *datastore.Spec `json:"spec"`
|
||||||
|
}{
|
||||||
|
Spec: spec,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateTenantRequest struct {
|
||||||
|
Label *string `json:"label" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) updateTenant(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tenantID, ok := getTenantID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
updateTenantReq := &UpdateTenantRequest{}
|
||||||
|
if ok := api.Bind(w, r, updateTenantReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]datastore.TenantUpdateOptionFunc, 0)
|
||||||
|
|
||||||
|
if updateTenantReq.Label != nil {
|
||||||
|
options = append(options, datastore.WithTenantUpdateLabel(*updateTenantReq.Label))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenant, err := m.tenantRepo.Update(
|
||||||
|
ctx,
|
||||||
|
tenantID,
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not update tenant", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Tenant *datastore.Tenant `json:"tenant"`
|
||||||
|
}{
|
||||||
|
Tenant: tenant,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,156 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/api"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrCodeForbidden api.ErrorCode = "forbidden"
|
|
||||||
|
|
||||||
func assertGlobalReadAccess(h http.Handler) http.Handler {
|
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
reqUser, ok := assertRequestUser(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch user := reqUser.(type) {
|
|
||||||
case *thirdparty.User:
|
|
||||||
role := user.Role()
|
|
||||||
if role == thirdparty.RoleReader || role == thirdparty.RoleWriter {
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case *agent.User:
|
|
||||||
// Agents dont have global read access
|
|
||||||
|
|
||||||
default:
|
|
||||||
logUnexpectedUserType(r.Context(), reqUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
forbidden(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.HandlerFunc(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertAgentWriteAccess(h http.Handler) http.Handler {
|
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
reqUser, ok := assertRequestUser(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch user := reqUser.(type) {
|
|
||||||
case *thirdparty.User:
|
|
||||||
role := user.Role()
|
|
||||||
if role == thirdparty.RoleWriter {
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case *agent.User:
|
|
||||||
if user.Agent().ID == agentID {
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
logUnexpectedUserType(r.Context(), reqUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
forbidden(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.HandlerFunc(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertAgentReadAccess(h http.Handler) http.Handler {
|
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
reqUser, ok := assertRequestUser(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch user := reqUser.(type) {
|
|
||||||
case *thirdparty.User:
|
|
||||||
role := user.Role()
|
|
||||||
if role == thirdparty.RoleReader || role == thirdparty.RoleWriter {
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case *agent.User:
|
|
||||||
if user.Agent().ID == agentID {
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
logUnexpectedUserType(r.Context(), reqUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
forbidden(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.HandlerFunc(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool) {
|
|
||||||
ctx := r.Context()
|
|
||||||
user, err := auth.CtxUser(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not retrieve user", logger.CapturedE(err))
|
|
||||||
|
|
||||||
forbidden(w, r)
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
forbidden(w, r)
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logger.Warn(r.Context(), "forbidden", logger.F("path", r.URL.Path))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logUnexpectedUserType(ctx context.Context, user auth.User) {
|
|
||||||
logger.Warn(
|
|
||||||
ctx, "unexpected user type",
|
|
||||||
logger.F("subject", user.Subject()),
|
|
||||||
logger.F("type", fmt.Sprintf("%T", user)),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -13,7 +13,13 @@ func (s *Server) initRepositories(ctx context.Context) error {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tenantRepo, err := setup.NewTenantRepository(ctx, s.conf.Database)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
s.agentRepo = agentRepo
|
s.agentRepo = agentRepo
|
||||||
|
s.tenantRepo = tenantRepo
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/config"
|
"forge.cadoles.com/Cadoles/emissary/internal/config"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/server/api"
|
||||||
"github.com/antonmedv/expr"
|
"github.com/antonmedv/expr"
|
||||||
"github.com/antonmedv/expr/vm"
|
"github.com/antonmedv/expr/vm"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -28,8 +28,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
conf config.ServerConfig
|
conf config.ServerConfig
|
||||||
agentRepo datastore.AgentRepository
|
agentRepo datastore.AgentRepository
|
||||||
|
tenantRepo datastore.TenantRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||||
|
@ -93,7 +94,7 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||||
|
|
||||||
router.Use(corsMiddleware.Handler)
|
router.Use(corsMiddleware.Handler)
|
||||||
|
|
||||||
thirdPartyAuth, err := s.getThirdPartyAuthenticator()
|
userAuth, err := s.getUserAuthenticator()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- errors.WithStack(err)
|
errs <- errors.WithStack(err)
|
||||||
|
|
||||||
|
@ -101,25 +102,14 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||||
}
|
}
|
||||||
|
|
||||||
router.Route("/api/v1", func(r chi.Router) {
|
router.Route("/api/v1", func(r chi.Router) {
|
||||||
r.Post("/register", s.registerAgent)
|
apiMount := api.NewMount(
|
||||||
|
s.agentRepo,
|
||||||
|
s.tenantRepo,
|
||||||
|
userAuth,
|
||||||
|
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||||
|
)
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
apiMount.Mount(r)
|
||||||
r.Use(auth.Middleware(
|
|
||||||
thirdPartyAuth,
|
|
||||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
|
||||||
))
|
|
||||||
|
|
||||||
r.Route("/agents", func(r chi.Router) {
|
|
||||||
r.With(assertGlobalReadAccess).Get("/", s.queryAgents)
|
|
||||||
r.With(assertAgentReadAccess).Get("/{agentID}", s.getAgent)
|
|
||||||
r.With(assertAgentWriteAccess).Put("/{agentID}", s.updateAgent)
|
|
||||||
r.With(assertAgentWriteAccess).Delete("/{agentID}", s.deleteAgent)
|
|
||||||
|
|
||||||
r.With(assertAgentReadAccess).Get("/{agentID}/specs", s.getAgentSpecs)
|
|
||||||
r.With(assertAgentWriteAccess).Post("/{agentID}/specs", s.updateSpec)
|
|
||||||
r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", s.deleteSpec)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.Info(ctx, "http server listening")
|
logger.Info(ctx, "http server listening")
|
||||||
|
@ -131,7 +121,7 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||||
logger.Info(ctx, "http server exiting")
|
logger.Info(ctx, "http server exiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getThirdPartyAuthenticator() (*thirdparty.Authenticator, error) {
|
func (s *Server) getUserAuthenticator() (*user.Authenticator, error) {
|
||||||
var localPublicKey jwk.Key
|
var localPublicKey jwk.Key
|
||||||
|
|
||||||
localAuth := s.conf.Auth.Local
|
localAuth := s.conf.Auth.Local
|
||||||
|
@ -153,7 +143,7 @@ func (s *Server) getThirdPartyAuthenticator() (*thirdparty.Authenticator, error)
|
||||||
localPublicKey = publicKey
|
localPublicKey = publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
var getRemoteKeySet thirdparty.GetKeySet
|
var getRemoteKeySet user.GetKeySet
|
||||||
|
|
||||||
remoteAuth := s.conf.Auth.Remote
|
remoteAuth := s.conf.Auth.Remote
|
||||||
if remoteAuth != nil {
|
if remoteAuth != nil {
|
||||||
|
@ -205,18 +195,16 @@ func (s *Server) getThirdPartyAuthenticator() (*thirdparty.Authenticator, error)
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return thirdparty.NewAuthenticator(getKeySet, getTokenRole, thirdparty.DefaultAcceptableSkew), nil
|
getTenantRole, err := s.createGetTokenTenantFunc()
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
|
|
||||||
rawRules := s.conf.Auth.RoleExtractionRules
|
|
||||||
rules := make([]*vm.Program, 0, len(rawRules))
|
|
||||||
|
|
||||||
type Env struct {
|
|
||||||
JWT map[string]any `expr:"jwt"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strFunc := expr.Function(
|
return user.NewAuthenticator(getKeySet, getTokenRole, getTenantRole, user.DefaultAcceptableSkew), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ruleFuncs = []expr.Option{
|
||||||
|
expr.Function(
|
||||||
"str",
|
"str",
|
||||||
func(params ...any) (any, error) {
|
func(params ...any) (any, error) {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
|
@ -230,14 +218,24 @@ func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.T
|
||||||
return builder.String(), nil
|
return builder.String(), nil
|
||||||
},
|
},
|
||||||
new(func(any) string),
|
new(func(any) string),
|
||||||
)
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
|
||||||
|
rawRules := s.conf.Auth.RoleExtractionRules
|
||||||
|
rules := make([]*vm.Program, 0, len(rawRules))
|
||||||
|
|
||||||
|
type Env struct {
|
||||||
|
JWT map[string]any `expr:"jwt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := append([]expr.Option{
|
||||||
|
expr.Env(Env{}),
|
||||||
|
expr.AsKind(reflect.String),
|
||||||
|
}, ruleFuncs...)
|
||||||
|
|
||||||
for _, rr := range rawRules {
|
for _, rr := range rawRules {
|
||||||
r, err := expr.Compile(rr,
|
r, err := expr.Compile(rr, opts...)
|
||||||
expr.Env(Env{}),
|
|
||||||
expr.AsKind(reflect.String),
|
|
||||||
strFunc,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "could not compile role extraction rule '%s'", rr)
|
return nil, errors.Wrapf(err, "could not compile role extraction rule '%s'", rr)
|
||||||
}
|
}
|
||||||
|
@ -276,6 +274,59 @@ func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.T
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) createGetTokenTenantFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
|
||||||
|
rawRules := s.conf.Auth.TenantExtractionRules
|
||||||
|
rules := make([]*vm.Program, 0, len(rawRules))
|
||||||
|
|
||||||
|
type Env struct {
|
||||||
|
JWT map[string]any `expr:"jwt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := append([]expr.Option{
|
||||||
|
expr.Env(Env{}),
|
||||||
|
expr.AsKind(reflect.String),
|
||||||
|
}, ruleFuncs...)
|
||||||
|
|
||||||
|
for _, rr := range rawRules {
|
||||||
|
r, err := expr.Compile(rr, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not compile role extraction rule '%s'", rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(ctx context.Context, token jwt.Token) (string, error) {
|
||||||
|
jwt, err := token.AsMap(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vm := vm.VM{}
|
||||||
|
|
||||||
|
for _, r := range rules {
|
||||||
|
result, err := vm.Run(r, Env{
|
||||||
|
JWT: jwt,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenant, ok := result.(string)
|
||||||
|
if !ok {
|
||||||
|
logger.Debug(ctx, "ignoring unexpected tenant extraction result", logger.F("result", result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tenant != "" {
|
||||||
|
return tenant, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("could not extract tenant from token")
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func New(funcs ...OptionFunc) *Server {
|
func New(funcs ...OptionFunc) *Server {
|
||||||
opt := defaultOption()
|
opt := defaultOption()
|
||||||
for _, fn := range funcs {
|
for _, fn := range funcs {
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/api"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
|
|
||||||
)
|
|
||||||
|
|
||||||
type updateSpecRequest struct {
|
|
||||||
spec.RawSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) updateSpec(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
updateSpecReq := &updateSpecRequest{}
|
|
||||||
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := spec.Validate(ctx, updateSpecReq); err != nil {
|
|
||||||
data := struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
var validationErr *spec.ValidationError
|
|
||||||
|
|
||||||
if errors.As(err, &validationErr) {
|
|
||||||
data.Message = validationErr.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not validate spec", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, data)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
spec, err := s.agentRepo.UpdateSpec(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
string(updateSpecReq.SpecName()),
|
|
||||||
updateSpecReq.SpecRevision(),
|
|
||||||
updateSpecReq.SpecData(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, datastore.ErrUnexpectedRevision) {
|
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeUnexpectedRevision, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not update spec", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Spec *datastore.Spec `json:"spec"`
|
|
||||||
}{
|
|
||||||
Spec: spec,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
specs, err := s.agentRepo.GetSpecs(ctx, agentID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not list specs", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Specs []*datastore.Spec `json:"specs"`
|
|
||||||
}{
|
|
||||||
Specs: specs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type deleteSpecRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSpecReq := &deleteSpecRequest{}
|
|
||||||
if ok := api.Bind(w, r, deleteSpecReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
err := s.agentRepo.DeleteSpec(
|
|
||||||
ctx,
|
|
||||||
agentID,
|
|
||||||
deleteSpecReq.Name,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not delete spec", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}{
|
|
||||||
Name: deleteSpecReq.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
|
||||||
rawSpecID := chi.URLParam(r, "")
|
|
||||||
|
|
||||||
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(r.Context(), "could not parse spec id", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return datastore.SpecID(specID), true
|
|
||||||
}
|
|
|
@ -67,3 +67,40 @@ func NewAgentRepository(ctx context.Context, conf config.DatabaseConfig) (datast
|
||||||
|
|
||||||
return agentRepository, nil
|
return agentRepository, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewTenantRepository(ctx context.Context, conf config.DatabaseConfig) (datastore.TenantRepository, error) {
|
||||||
|
driver := string(conf.Driver)
|
||||||
|
dsn := string(conf.DSN)
|
||||||
|
|
||||||
|
var tenantRepository datastore.TenantRepository
|
||||||
|
|
||||||
|
logger.Debug(ctx, "initializing tenant repository", logger.F("driver", driver), logger.F("dsn", dsn))
|
||||||
|
|
||||||
|
switch driver {
|
||||||
|
case config.DatabaseDriverPostgres:
|
||||||
|
// TODO
|
||||||
|
// pool, err := openPostgresPool(ctx, dsn)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, errors.WithStack(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// entryRepository = postgres.NewEntryRepository(pool)
|
||||||
|
case config.DatabaseDriverSQLite:
|
||||||
|
url, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open(driver, url.Host+url.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantRepository = sqlite.NewTenantRepository(db, 5)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("unsupported database driver '%s'", driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenantRepository, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
ALTER TABLE agents RENAME TO _agents;
|
||||||
|
|
||||||
|
CREATE TABLE agents
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
thumbprint TEXT UNIQUE,
|
||||||
|
keyset TEXT,
|
||||||
|
metadata TEXT,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
label TEXT DEFAULT "",
|
||||||
|
contacted_at datetime
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO agents SELECT id, thumbprint, keyset, metadata, status, created_at, updated_at, label, contacted_at FROM _agents;
|
||||||
|
DROP TABLE _agents;
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
ALTER TABLE specs RENAME TO _specs;
|
||||||
|
|
||||||
|
CREATE TABLE specs
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
thumbprint TEXT UNIQUE,
|
||||||
|
keyset TEXT,
|
||||||
|
metadata TEXT,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
label TEXT DEFAULT ""
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO specs SELECT id, agent_id, name, revision, data, created_at, updated_at FROM _specs;
|
||||||
|
DROP TABLE _specs;
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
DROP TABLE tenants;
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
CREATE TABLE tenants (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add foreign key to agents
|
||||||
|
|
||||||
|
ALTER TABLE agents RENAME TO _agents;
|
||||||
|
|
||||||
|
CREATE TABLE agents
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
thumbprint TEXT UNIQUE,
|
||||||
|
keyset TEXT,
|
||||||
|
metadata TEXT,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
label TEXT DEFAULT "",
|
||||||
|
contacted_at datetime,
|
||||||
|
tenant_id TEXT,
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO agents SELECT id, thumbprint, keyset, metadata, status, created_at, updated_at, label, contacted_at, 0 FROM _agents;
|
||||||
|
|
||||||
|
DROP TABLE _agents;
|
||||||
|
|
||||||
|
-- Add foreign key to specs
|
||||||
|
|
||||||
|
ALTER TABLE specs RENAME TO _specs;
|
||||||
|
|
||||||
|
CREATE TABLE specs
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
agent_id INTEGER,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
revision INTEGER DEFAULT 0,
|
||||||
|
data TEXT,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
tenant_id TEXT,
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants (id),
|
||||||
|
FOREIGN KEY (agent_id) REFERENCES agents (id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(agent_id, name) ON CONFLICT REPLACE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO specs SELECT id, agent_id, name, revision, data, created_at, updated_at, 0 FROM _specs;
|
||||||
|
|
||||||
|
DROP TABLE _specs;
|
|
@ -8,7 +8,6 @@ tmp/config.yml
|
||||||
prep: make tmp/server.yml
|
prep: make tmp/server.yml
|
||||||
prep: make tmp/agent.yml
|
prep: make tmp/agent.yml
|
||||||
prep: make run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server database migrate"
|
prep: make run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server database migrate"
|
||||||
prep: make .emissary-token
|
|
||||||
daemon: make run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server run"
|
daemon: make run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server run"
|
||||||
daemon: make run-emissary-agent EMISSARY_CMD="--debug --config tmp/agent.yml agent run"
|
daemon: make run-emissary-agent EMISSARY_CMD="--debug --config tmp/agent.yml agent run"
|
||||||
}
|
}
|
|
@ -15,6 +15,8 @@ type (
|
||||||
type (
|
type (
|
||||||
AgentID = datastore.AgentID
|
AgentID = datastore.AgentID
|
||||||
Agent = datastore.Agent
|
Agent = datastore.Agent
|
||||||
|
TenantID = datastore.TenantID
|
||||||
|
Tenant = datastore.Tenant
|
||||||
AgentStatus = datastore.AgentStatus
|
AgentStatus = datastore.AgentStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) ClaimAgent(ctx context.Context, agentThumbprint string, funcs ...OptionFunc) (*Agent, error) {
|
||||||
|
response := withResponse[struct {
|
||||||
|
Agent *Agent `json:"agent"`
|
||||||
|
}]()
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"thumbprint": agentThumbprint,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.apiPost(ctx, "/api/v1/agents/claim", payload, &response, funcs...); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != nil {
|
||||||
|
return nil, errors.WithStack(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data.Agent, nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue