Resources segregation by tenant #20
|
@ -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
|
||||||
|
|
24
Makefile
24
Makefile
|
@ -122,19 +122,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 --tenant '00000000-0000-0000-0000-000000000000'"
|
$(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
|
||||||
|
|
||||||
claim-agent:
|
claim-agent: .emissary-token
|
||||||
go run ./cmd/server api agent claim --agent-thumbprint $(shell go run ./cmd/agent agent show-thumbprint)
|
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml client agent claim --agent-thumbprint $(shell go run ./cmd/agent agent show-thumbprint)"
|
||||||
|
|
||||||
load-sample-specs:
|
load-sample-specs: .emissary-token
|
||||||
cat misc/spec-samples/app.emissary.cadoles.com.json | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.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 | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.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 | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.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 | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name uci.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)
|
||||||
|
|
20
README.md
20
README.md
|
@ -6,6 +6,26 @@ 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 a second terminal, create a api token, an admin token and a tenant
|
||||||
|
make .emissary-token
|
||||||
|
|
||||||
|
# Claim the agent for your newly created tenant
|
||||||
|
make claim-agent
|
||||||
|
|
||||||
|
# Query you agents
|
||||||
|
./bin/server client agent query
|
||||||
|
```
|
||||||
|
|
||||||
## 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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
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 {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package thirdparty
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -1,4 +1,4 @@
|
||||||
package thirdparty
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -1,4 +1,4 @@
|
||||||
package thirdparty
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
|
@ -10,12 +10,13 @@ type Role string
|
||||||
const (
|
const (
|
||||||
RoleWriter Role = "writer"
|
RoleWriter Role = "writer"
|
||||||
RoleReader Role = "reader"
|
RoleReader Role = "reader"
|
||||||
|
RoleAdmin Role = "admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isValidRole(r string) bool {
|
func isValidRole(r string) bool {
|
||||||
rr := Role(r)
|
rr := Role(r)
|
||||||
|
|
||||||
return rr == RoleWriter || rr == RoleReader
|
return rr == RoleWriter || rr == RoleReader || rr == RoleAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
|
@ -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(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,8 +3,8 @@ 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/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"
|
|
@ -3,8 +3,8 @@ 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/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"
|
|
@ -3,9 +3,9 @@ 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/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
|
@ -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,9 +3,9 @@ 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/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"
|
|
@ -10,7 +10,6 @@ func agentHints(outputMode format.OutputMode) format.Hints {
|
||||||
OutputMode: outputMode,
|
OutputMode: outputMode,
|
||||||
Props: []format.Prop{
|
Props: []format.Prop{
|
||||||
format.NewProp("ID", "ID"),
|
format.NewProp("ID", "ID"),
|
||||||
format.NewProp("TenantID", "Tenant", table.WithCompactModeMaxColumnWidth(8)),
|
|
||||||
format.NewProp("Label", "Label"),
|
format.NewProp("Label", "Label"),
|
||||||
format.NewProp("Thumbprint", "Thumbprint"),
|
format.NewProp("Thumbprint", "Thumbprint"),
|
||||||
format.NewProp("Status", "Status"),
|
format.NewProp("Status", "Status"),
|
|
@ -3,8 +3,8 @@ 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/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,9 +3,9 @@ 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/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"
|
|
@ -3,9 +3,9 @@ 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/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"
|
|
@ -4,9 +4,9 @@ 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/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"
|
|
@ -3,9 +3,9 @@ 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/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"
|
|
@ -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,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,17 @@
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,12 +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/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"
|
||||||
)
|
)
|
||||||
|
@ -22,18 +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{
|
&cli.StringFlag{
|
||||||
Name: "tenant",
|
Name: "tenant",
|
||||||
Usage: "associate `TENANT` to the token",
|
Usage: "associate `TENANT` to the token",
|
||||||
Required: true,
|
Value: "00000000-0000-0000-0000-000000000000",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "output",
|
Name: "output",
|
||||||
|
@ -64,7 +64,7 @@ func CreateTokenCommand() *cli.Command {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := thirdparty.GenerateToken(ctx.Context, key, datastore.TenantID(tenant), 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 {
|
||||||
|
@ -36,10 +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{
|
TenantExtractionRules: []string{
|
||||||
fmt.Sprintf("jwt.%s != nil ? str(jwt.%s) : ''", thirdparty.DefaultTenantKey, thirdparty.DefaultTenantKey),
|
fmt.Sprintf("jwt.%s != nil ? str(jwt.%s) : ''", user.DefaultTenantKey, user.DefaultTenantKey),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentRepository struct {
|
type AgentRepository struct {
|
||||||
db *sql.DB
|
repository
|
||||||
sqliteBusyRetryMaxAttempts int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach implements datastore.AgentRepository.
|
// Attach implements datastore.AgentRepository.
|
||||||
|
@ -652,89 +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{}
|
||||||
|
|
|
@ -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,202 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,22 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type TenantUpdateOptionFunc func(*TenantUpdateOptions)
|
||||||
|
|
||||||
|
type TenantUpdateOptions struct {
|
||||||
|
Label *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTenantUpdateLabel(label string) TenantUpdateOptionFunc {
|
||||||
|
return func(opts *TenantUpdateOptions) {
|
||||||
|
opts.Label = &label
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
"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/datastore"
|
"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"
|
||||||
|
@ -16,101 +16,99 @@ import (
|
||||||
|
|
||||||
var ErrCodeForbidden api.ErrorCode = "forbidden"
|
var ErrCodeForbidden api.ErrorCode = "forbidden"
|
||||||
|
|
||||||
func assertGlobalReadAccess(h http.Handler) http.Handler {
|
func assertQueryAccess(h http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
return assertAuthz(
|
||||||
reqUser, ok := assertRequestUser(w, r)
|
h,
|
||||||
if !ok {
|
assertOneOfRoles(user.RoleReader, user.RoleWriter, user.RoleAdmin),
|
||||||
return
|
nil,
|
||||||
}
|
)
|
||||||
|
|
||||||
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 {
|
func assertUserWithWriteAccess(h http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
return assertAuthz(
|
||||||
reqUser, ok := assertRequestUser(w, r)
|
h,
|
||||||
if !ok {
|
assertOneOfRoles(user.RoleWriter, user.RoleAdmin),
|
||||||
return
|
nil,
|
||||||
}
|
)
|
||||||
|
|
||||||
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 {
|
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),
|
||||||
|
assertTenant(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAdminOrTenantWriteAccess(h http.Handler) http.Handler {
|
||||||
|
return assertAuthz(
|
||||||
|
h,
|
||||||
|
assertOneOfUser(
|
||||||
|
assertOneOfRoles(user.RoleAdmin),
|
||||||
|
assertAllOfUser(
|
||||||
|
assertOneOfRoles(user.RoleWriter),
|
||||||
|
assertTenant(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAuthz(h http.Handler, assertUser assertUser, assertAgent assertAgent) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
reqUser, ok := assertRequestUser(w, r)
|
reqUser, ok := assertRequestUser(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
agentID, ok := getAgentID(w, r)
|
switch u := reqUser.(type) {
|
||||||
if !ok {
|
case *user.User:
|
||||||
return
|
if assertUser != nil {
|
||||||
}
|
if ok := assertUser(w, r, u); ok {
|
||||||
|
|
||||||
switch user := reqUser.(type) {
|
|
||||||
case *thirdparty.User:
|
|
||||||
role := user.Role()
|
|
||||||
if role == thirdparty.RoleReader || role == thirdparty.RoleWriter {
|
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case *agent.User:
|
case *agent.User:
|
||||||
if user.Agent().ID == agentID {
|
if assertAgent != nil {
|
||||||
|
if ok := assertAgent(w, r, u); ok {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
logUnexpectedUserType(r.Context(), reqUser)
|
logUnexpectedUserType(r.Context(), reqUser)
|
||||||
}
|
}
|
||||||
|
@ -119,6 +117,78 @@ func assertAgentReadAccess(h http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.HandlerFunc(fn)
|
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 assertTenant() 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) {
|
func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool) {
|
||||||
|
|
|
@ -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,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,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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
||||||
rawSpecID := chi.URLParam(r, "")
|
rawSpecID := chi.URLParam(r, "specID")
|
||||||
|
|
||||||
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,6 +51,22 @@ func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool)
|
||||||
return datastore.SpecID(specID), true
|
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) {
|
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
||||||
rawValue := r.URL.Query().Get(param)
|
rawValue := r.URL.Query().Get(param)
|
||||||
if rawValue != "" {
|
if rawValue != "" {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
type Mount struct {
|
type Mount struct {
|
||||||
agentRepo datastore.AgentRepository
|
agentRepo datastore.AgentRepository
|
||||||
|
tenantRepo datastore.TenantRepository
|
||||||
authenticators []auth.Authenticator
|
authenticators []auth.Authenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,17 +24,24 @@ func (m *Mount) Mount(r chi.Router) {
|
||||||
r.Use(auth.Middleware(m.authenticators...))
|
r.Use(auth.Middleware(m.authenticators...))
|
||||||
|
|
||||||
r.Route("/agents", func(r chi.Router) {
|
r.Route("/agents", func(r chi.Router) {
|
||||||
r.Post("/claim", m.claimAgent)
|
r.With(assertUserWithWriteAccess).Post("/claim", m.claimAgent)
|
||||||
|
|
||||||
r.With(assertGlobalReadAccess).Get("/", m.queryAgents)
|
r.With(assertQueryAccess).Get("/", m.queryAgents)
|
||||||
|
|
||||||
r.With(assertAgentReadAccess).Get("/{agentID}", m.getAgent)
|
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}", m.getAgent)
|
||||||
r.With(assertAgentWriteAccess).Put("/{agentID}", m.updateAgent)
|
r.With(assertAgentOrUserWithWriteAccess).Put("/{agentID}", m.updateAgent)
|
||||||
r.With(assertAgentWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
r.With(assertUserWithWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
||||||
|
|
||||||
r.With(assertAgentReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
||||||
r.With(assertAgentWriteAccess).Post("/{agentID}/specs", m.updateSpec)
|
r.With(assertUserWithWriteAccess).Post("/{agentID}/specs", m.updateSpec)
|
||||||
r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", m.deleteSpec)
|
r.With(assertUserWithWriteAccess).Delete("/{agentID}/specs", m.deleteSpec)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/tenants", func(r chi.Router) {
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -42,6 +50,6 @@ func (m *Mount) notFound(w http.ResponseWriter, r *http.Request) {
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMount(agentRepo datastore.AgentRepository, authenticators ...auth.Authenticator) *Mount {
|
func NewMount(agentRepo datastore.AgentRepository, tenantRepo datastore.TenantRepository, authenticators ...auth.Authenticator) *Mount {
|
||||||
return &Mount{agentRepo, authenticators}
|
return &Mount{agentRepo, tenantRepo, authenticators}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,12 @@ func (m *Mount) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
options...,
|
options...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
|
@ -30,6 +30,7 @@ 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)
|
||||||
|
|
||||||
|
@ -103,7 +104,8 @@ 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) {
|
||||||
apiMount := api.NewMount(
|
apiMount := api.NewMount(
|
||||||
s.agentRepo,
|
s.agentRepo,
|
||||||
thirdPartyAuth,
|
s.tenantRepo,
|
||||||
|
userAuth,
|
||||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -119,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
|
||||||
|
@ -141,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 {
|
||||||
|
@ -198,7 +200,7 @@ func (s *Server) getThirdPartyAuthenticator() (*thirdparty.Authenticator, error)
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return thirdparty.NewAuthenticator(getKeySet, getTokenRole, getTenantRole, thirdparty.DefaultAcceptableSkew), nil
|
return user.NewAuthenticator(getKeySet, getTokenRole, getTenantRole, user.DefaultAcceptableSkew), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ruleFuncs = []expr.Option{
|
var ruleFuncs = []expr.Option{
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -5,18 +5,6 @@ CREATE TABLE tenants (
|
||||||
updated_at datetime NOT NULL
|
updated_at datetime NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Insert default tenant
|
|
||||||
|
|
||||||
INSERT INTO tenants
|
|
||||||
( id, label, created_at, updated_at )
|
|
||||||
VALUES (
|
|
||||||
'00000000-0000-0000-0000-000000000000',
|
|
||||||
'Default',
|
|
||||||
date('now'),
|
|
||||||
date('now')
|
|
||||||
)
|
|
||||||
;
|
|
||||||
|
|
||||||
-- Add foreign key to agents
|
-- Add foreign key to agents
|
||||||
|
|
||||||
ALTER TABLE agents RENAME TO _agents;
|
ALTER TABLE agents RENAME TO _agents;
|
||||||
|
|
|
@ -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,28 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/server/api"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) CreateTenant(ctx context.Context, label string, funcs ...OptionFunc) (*Tenant, error) {
|
||||||
|
response := withResponse[struct {
|
||||||
|
Tenant *Tenant `json:"tenant"`
|
||||||
|
}]()
|
||||||
|
|
||||||
|
payload := api.CreateTenantRequest{
|
||||||
|
Label: label,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.apiPost(ctx, "/api/v1/tenants", payload, &response, funcs...); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != nil {
|
||||||
|
return nil, errors.WithStack(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data.Tenant, nil
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) GetTenant(ctx context.Context, tenantID TenantID, funcs ...OptionFunc) (*Tenant, error) {
|
||||||
|
response := withResponse[struct {
|
||||||
|
Tenant *Tenant `json:"tenant"`
|
||||||
|
}]()
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/api/v1/tenants/%s", tenantID)
|
||||||
|
|
||||||
|
if err := c.apiGet(ctx, path, &response, funcs...); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != nil {
|
||||||
|
return nil, errors.WithStack(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data.Tenant, nil
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, funcs ...UpdateAgentOptionFunc) (*datastore.Agent, error) {
|
func (c *Client) UpdateAgent(ctx context.Context, agentID AgentID, funcs ...UpdateAgentOptionFunc) (*datastore.Agent, error) {
|
||||||
opts := &UpdateAgentOptions{}
|
opts := &UpdateAgentOptions{}
|
||||||
for _, fn := range funcs {
|
for _, fn := range funcs {
|
||||||
fn(opts)
|
fn(opts)
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateTenantOptions struct {
|
||||||
|
Label *string
|
||||||
|
Options []OptionFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTenantOptionFunc func(*UpdateTenantOptions)
|
||||||
|
|
||||||
|
func WithTenantLabel(label string) UpdateTenantOptionFunc {
|
||||||
|
return func(opts *UpdateTenantOptions) {
|
||||||
|
opts.Label = &label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithUpdateTenantOptions(funcs ...OptionFunc) UpdateTenantOptionFunc {
|
||||||
|
return func(opts *UpdateTenantOptions) {
|
||||||
|
opts.Options = funcs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpdateTenant(ctx context.Context, tenantID TenantID, funcs ...UpdateTenantOptionFunc) (*Tenant, error) {
|
||||||
|
opts := &UpdateTenantOptions{}
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]any{}
|
||||||
|
|
||||||
|
if opts.Label != nil {
|
||||||
|
payload["label"] = *opts.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
response := withResponse[struct {
|
||||||
|
Tenant *datastore.Tenant `json:"tenant"`
|
||||||
|
}]()
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/api/v1/tenants/%s", tenantID)
|
||||||
|
|
||||||
|
if opts.Options == nil {
|
||||||
|
opts.Options = make([]OptionFunc, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.apiPut(ctx, path, payload, &response, opts.Options...); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != nil {
|
||||||
|
return nil, errors.WithStack(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data.Tenant, nil
|
||||||
|
}
|
Loading…
Reference in New Issue