feat: basic authorization model
This commit is contained in:
@ -30,7 +30,6 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(a.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
logger.Info(ctx, "generating token")
|
||||
token, err := agent.GenerateToken(a.privateKey, a.thumbprint)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -11,16 +11,21 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type serverEntry struct {
|
||||
SpecHash uint64
|
||||
Server *Server
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
currentSpecRevision int
|
||||
client *http.Client
|
||||
downloadDir string
|
||||
dataDir string
|
||||
servers map[string]*Server
|
||||
client *http.Client
|
||||
downloadDir string
|
||||
dataDir string
|
||||
servers map[string]*serverEntry
|
||||
}
|
||||
|
||||
// Name implements node.Controller.
|
||||
@ -34,9 +39,9 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
|
||||
if err := state.GetSpec(app.NameApp, appSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find app spec, stopping all remaining apps")
|
||||
logger.Info(ctx, "could not find app spec")
|
||||
|
||||
c.stopAllApps(ctx)
|
||||
c.stopAllApps(ctx, appSpec)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -46,22 +51,20 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
|
||||
logger.Info(ctx, "retrieved spec", logger.F("spec", appSpec.SpecName()), logger.F("revision", appSpec.SpecRevision()))
|
||||
|
||||
if c.currentSpecRevision == appSpec.SpecRevision() {
|
||||
logger.Info(ctx, "spec revision did not change, doing nothing")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
c.updateApps(ctx, appSpec)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopAllApps(ctx context.Context) {
|
||||
for appID, server := range c.servers {
|
||||
func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
|
||||
if len(c.servers) > 0 {
|
||||
logger.Info(ctx, "stopping all apps")
|
||||
}
|
||||
|
||||
for appID, entry := range c.servers {
|
||||
logger.Info(ctx, "stopping app", logger.F("appID", appID))
|
||||
|
||||
if err := server.Stop(); err != nil {
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
logger.Error(
|
||||
ctx, "error while stopping app",
|
||||
logger.F("appID", appID),
|
||||
@ -74,17 +77,15 @@ func (c *Controller) stopAllApps(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) {
|
||||
hadError := false
|
||||
|
||||
// Stop and remove obsolete apps
|
||||
for appID, server := range c.servers {
|
||||
for appID, entry := range c.servers {
|
||||
if _, exists := spec.Apps[appID]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info(ctx, "stopping app", logger.F("appID", appID))
|
||||
|
||||
if err := server.Stop(); err != nil {
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
logger.Error(
|
||||
ctx, "error while stopping app",
|
||||
logger.F("gatewayID", appID),
|
||||
@ -92,8 +93,6 @@ func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) {
|
||||
)
|
||||
|
||||
delete(c.servers, appID)
|
||||
|
||||
hadError = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,20 +102,17 @@ func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) {
|
||||
|
||||
if err := c.updateApp(ctx, appID, appSpec); err != nil {
|
||||
logger.Error(appCtx, "could not update app", logger.E(errors.WithStack(err)))
|
||||
|
||||
hadError = true
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !hadError {
|
||||
c.currentSpecRevision = spec.SpecRevision()
|
||||
logger.Info(ctx, "updating current spec revision", logger.F("revision", c.currentSpecRevision))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry) error {
|
||||
func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry) (err error) {
|
||||
newAppSpecHash, err := hashstructure.Hash(appSpec, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
bundle, sha256sum, err := c.ensureAppBundle(ctx, appID, appSpec)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not download app bundle")
|
||||
@ -127,7 +123,9 @@ func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.Ap
|
||||
return errors.Wrap(err, "could not retrieve app data dir")
|
||||
}
|
||||
|
||||
server, exists := c.servers[appID]
|
||||
var entry *serverEntry
|
||||
|
||||
entry, exists := c.servers[appID]
|
||||
if !exists {
|
||||
logger.Info(ctx, "app currently not running")
|
||||
} else if sha256sum != appSpec.SHA256Sum {
|
||||
@ -137,35 +135,54 @@ func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.Ap
|
||||
logger.F("specHash", appSpec.SHA256Sum),
|
||||
)
|
||||
|
||||
if err := server.Stop(); err != nil {
|
||||
if err := entry.Server.Stop(); err != nil {
|
||||
return errors.Wrap(err, "could not stop app")
|
||||
}
|
||||
|
||||
server = nil
|
||||
entry = nil
|
||||
}
|
||||
|
||||
if server == nil {
|
||||
if entry == nil {
|
||||
dbFile := filepath.Join(dataDir, appID+".sqlite")
|
||||
db, err := sqlite.Open(dbFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not opend database file '%s'", dbFile)
|
||||
}
|
||||
|
||||
server = NewServer(bundle, db)
|
||||
c.servers[appID] = server
|
||||
entry = &serverEntry{
|
||||
Server: NewServer(bundle, db),
|
||||
SpecHash: 0,
|
||||
}
|
||||
|
||||
c.servers[appID] = entry
|
||||
}
|
||||
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
)
|
||||
specChanged := newAppSpecHash != entry.SpecHash
|
||||
|
||||
if err := server.Start(ctx, appSpec.Address); err != nil {
|
||||
if entry.Server.Running() && !specChanged {
|
||||
return nil
|
||||
}
|
||||
|
||||
if specChanged && entry.SpecHash != 0 {
|
||||
logger.Info(
|
||||
ctx, "restarting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
)
|
||||
} else {
|
||||
logger.Info(
|
||||
ctx, "starting app",
|
||||
logger.F("address", appSpec.Address),
|
||||
)
|
||||
}
|
||||
|
||||
if err := entry.Server.Start(ctx, appSpec.Address); err != nil {
|
||||
delete(c.servers, appID)
|
||||
|
||||
return errors.Wrap(err, "could not start app")
|
||||
}
|
||||
|
||||
entry.SpecHash = newAppSpecHash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -275,11 +292,10 @@ func NewController(funcs ...OptionFunc) *Controller {
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
client: opts.Client,
|
||||
downloadDir: opts.DownloadDir,
|
||||
dataDir: opts.DataDir,
|
||||
currentSpecRevision: -1,
|
||||
servers: make(map[string]*Server),
|
||||
client: opts.Client,
|
||||
downloadDir: opts.DownloadDir,
|
||||
dataDir: opts.DataDir,
|
||||
servers: make(map[string]*serverEntry),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,30 +4,35 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
bundle bundle.Bundle
|
||||
db *sql.DB
|
||||
server *http.Server
|
||||
bundle bundle.Bundle
|
||||
db *sql.DB
|
||||
server *http.Server
|
||||
serverMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context, addr string) error {
|
||||
func (s *Server) Start(ctx context.Context, addr string) (err error) {
|
||||
if s.server != nil {
|
||||
if err := s.Stop(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -58,6 +63,18 @@ func (s *Server) Start(ctx context.Context, addr string) error {
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
if err, ok := recovered.(error); ok {
|
||||
logger.Error(ctx, err.Error(), logger.E(errors.WithStack(err)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
panic(recovered)
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if err := s.Stop(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
@ -69,18 +86,29 @@ func (s *Server) Start(ctx context.Context, addr string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
s.serverMutex.Lock()
|
||||
s.server = server
|
||||
s.serverMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Running() bool {
|
||||
s.serverMutex.RLock()
|
||||
defer s.serverMutex.RUnlock()
|
||||
|
||||
return s.server != nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
if s.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
s.serverMutex.Lock()
|
||||
s.server = nil
|
||||
s.serverMutex.Unlock()
|
||||
}()
|
||||
|
||||
if err := s.server.Close(); err != nil {
|
||||
@ -100,20 +128,18 @@ func (s *Server) getAppModules(bus bus.Bus, ds storage.DocumentStore, bs storage
|
||||
module.RPCModuleFactory(bus),
|
||||
module.StoreModuleFactory(ds),
|
||||
module.BlobModuleFactory(bus, bs),
|
||||
// module.Extends(
|
||||
// auth.ModuleFactory(
|
||||
// auth.WithJWT(dummyKeyFunc),
|
||||
// ),
|
||||
// func(o *goja.Object) {
|
||||
// if err := o.Set("CLAIM_ROLE", "role"); err != nil {
|
||||
// panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
// }
|
||||
module.Extends(
|
||||
auth.ModuleFactory(),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_ROLE", "role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
// if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||
// panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,22 +21,15 @@ func (c *Controller) Name() string {
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
cl := agent.Client(ctx)
|
||||
|
||||
agents, _, err := cl.QueryAgents(
|
||||
agent, err := cl.GetAgent(
|
||||
ctx,
|
||||
client.WithQueryAgentsLimit(1),
|
||||
client.WithQueryAgentsID(state.AgentID()),
|
||||
state.AgentID(),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(agents) == 0 {
|
||||
logger.Error(ctx, "could not find remote matching agent")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.reconcileAgent(ctx, cl, state, agents[0]); err != nil {
|
||||
if err := c.reconcileAgent(ctx, cl, state, agent); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,8 @@ const (
|
||||
contextKeyUser contextKey = "user"
|
||||
)
|
||||
|
||||
func CtxUser(ctx context.Context) (*User, error) {
|
||||
user, ok := ctx.Value(contextKeyUser).(*User)
|
||||
func CtxUser(ctx context.Context) (User, error) {
|
||||
user, ok := ctx.Value(contextKeyUser).(User)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected user type: expected '%T', got '%T'", new(User), ctx.Value(contextKeyUser))
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package user
|
||||
package thirdparty
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package user
|
||||
package thirdparty
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package user
|
||||
package thirdparty
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
|
27
internal/client/delete_agent.go
Normal file
27
internal/client/delete_agent.go
Normal file
@ -0,0 +1,27 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) DeleteAgent(ctx context.Context, agentID datastore.AgentID, funcs ...OptionFunc) (datastore.AgentID, error) {
|
||||
response := withResponse[struct {
|
||||
AgentID int64 `json:"agentId"`
|
||||
}]()
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d", agentID)
|
||||
|
||||
if err := c.apiDelete(ctx, path, nil, &response, funcs...); err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if response.Error != nil {
|
||||
return 0, errors.WithStack(response.Error)
|
||||
}
|
||||
|
||||
return datastore.AgentID(response.Data.AgentID), nil
|
||||
}
|
34
internal/client/delete_agent_spec.go
Normal file
34
internal/client/delete_agent_spec.go
Normal file
@ -0,0 +1,34 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) DeleteAgentSpec(ctx context.Context, agentID datastore.AgentID, name spec.Name, funcs ...OptionFunc) (spec.Name, error) {
|
||||
payload := struct {
|
||||
Name spec.Name `json:"name"`
|
||||
}{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
response := withResponse[struct {
|
||||
Name spec.Name `json:"name"`
|
||||
}]()
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
||||
|
||||
if err := c.apiDelete(ctx, path, payload, &response, funcs...); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if response.Error != nil {
|
||||
return "", errors.WithStack(response.Error)
|
||||
}
|
||||
|
||||
return response.Data.Name, nil
|
||||
}
|
56
internal/command/api/agent/delete.go
Normal file
56
internal/command/api/agent/delete.go
Normal file
@ -0,0 +1,56 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/client"
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func DeleteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete agent",
|
||||
Flags: agentFlag.WithAgentFlags(),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
|
||||
token, err := clientFlag.GetToken(baseFlags)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
agentID, err := agentFlag.AssertAgentID(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
agentID, err = client.DeleteAgent(ctx.Context, agentID)
|
||||
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.AgentID `json:"id"`
|
||||
}{
|
||||
ID: agentID,
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ func Root() *cli.Command {
|
||||
CountCommand(),
|
||||
UpdateCommand(),
|
||||
GetCommand(),
|
||||
DeleteCommand(),
|
||||
spec.Root(),
|
||||
},
|
||||
}
|
||||
|
67
internal/command/api/agent/spec/delete.go
Normal file
67
internal/command/api/agent/spec/delete.go
Normal file
@ -0,0 +1,67 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/client"
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func DeleteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete spec",
|
||||
|
||||
Flags: agentFlag.WithAgentFlags(
|
||||
&cli.StringFlag{
|
||||
Name: "spec-name",
|
||||
Usage: "use `NAME` as specification's name",
|
||||
},
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
|
||||
token, err := clientFlag.GetToken(baseFlags)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
agentID, err := agentFlag.AssertAgentID(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
specName, err := assertSpecName(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
specName, err = client.DeleteAgentSpec(ctx.Context, agentID, specName)
|
||||
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 {
|
||||
Name spec.Name `json:"name"`
|
||||
}{
|
||||
Name: specName,
|
||||
}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ func Root() *cli.Command {
|
||||
Subcommands: []*cli.Command{
|
||||
GetCommand(),
|
||||
UpdateCommand(),
|
||||
DeleteCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -27,11 +27,11 @@ func UpdateCommand() *cli.Command {
|
||||
Flags: agentFlag.WithAgentFlags(
|
||||
&cli.StringFlag{
|
||||
Name: "spec-name",
|
||||
Usage: "use `NAME` as spec name",
|
||||
Usage: "use `NAME` as specification's name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "spec-data",
|
||||
Usage: "use `DATA` as spec data, '-' to read from STDIN",
|
||||
Usage: "use `DATA` as specification's data, '-' to read from STDIN",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-patch",
|
||||
|
@ -35,11 +35,11 @@ func ComposeFlags(flags ...cli.Flag) []cli.Flag {
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "use `TOKEN` as authentification token",
|
||||
Usage: "use `TOKEN` as authentication token",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token-file",
|
||||
Usage: "use `TOKEN_FILE` as file containing the authentification token",
|
||||
Usage: "use `TOKEN_FILE` as file containing the authentication token",
|
||||
Value: ".emissary-token",
|
||||
TakesFile: true,
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ package auth
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
@ -14,12 +14,12 @@ import (
|
||||
func CreateTokenCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create-token",
|
||||
Usage: "Create a new authentification token",
|
||||
Usage: "Create a new authentication token",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "role",
|
||||
Usage: fmt.Sprintf("associate `ROLE` to the token (available: %v)", []user.Role{user.RoleReader, user.RoleWriter}),
|
||||
Value: string(user.RoleReader),
|
||||
Usage: fmt.Sprintf("associate `ROLE` to the token (available: %v)", []thirdparty.Role{thirdparty.RoleReader, thirdparty.RoleWriter}),
|
||||
Value: string(thirdparty.RoleReader),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "subject",
|
||||
@ -41,7 +41,7 @@ func CreateTokenCommand() *cli.Command {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := user.GenerateToken(ctx.Context, key, string(conf.Server.Issuer), subject, user.Role(role))
|
||||
token, err := thirdparty.GenerateToken(ctx.Context, key, string(conf.Server.Issuer), subject, thirdparty.Role(role))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -269,9 +269,21 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet
|
||||
|
||||
// Delete implements datastore.AgentRepository
|
||||
func (r *AgentRepository) Delete(ctx context.Context, id datastore.AgentID) error {
|
||||
query := `DELETE FROM agents WHERE id = $1`
|
||||
err := r.withTx(ctx, func(tx *sql.Tx) error {
|
||||
query := `DELETE FROM agents WHERE id = $1`
|
||||
_, err := r.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, id)
|
||||
query = `DELETE FROM specs WHERE agent_id = $1`
|
||||
_, err = r.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
||||
ErrCodeNotFound api.ErrorCode = "not-found"
|
||||
ErrInvalidSignature api.ErrorCode = "invalid-signature"
|
||||
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
||||
ErrCodeNotFound api.ErrorCode = "not-found"
|
||||
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
||||
ErrCodeConflict api.ErrorCode = "conflict"
|
||||
)
|
||||
|
||||
type registerAgentRequest struct {
|
||||
@ -46,6 +47,8 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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 {
|
||||
logger.Error(ctx, "could not validate signature", logger.E(errors.WithStack(err)))
|
||||
@ -55,8 +58,8 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if !validSignature {
|
||||
logger.Error(ctx, "invalid signature", logger.F("signature", registerAgentReq.Signature))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, ErrInvalidSignature, nil)
|
||||
logger.Error(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature))
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||
|
||||
return
|
||||
}
|
||||
@ -97,6 +100,28 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
agentID := agents[0].ID
|
||||
|
||||
agent, err = s.agentRepo.Get(ctx, agentID)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not retrieve agent",
|
||||
logger.E(errors.WithStack(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 {
|
||||
logger.Error(ctx, "could not validate signature using previous keyset", logger.E(errors.WithStack(err)))
|
||||
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent, err = s.agentRepo.Update(
|
||||
ctx, agents[0].ID,
|
||||
datastore.WithAgentUpdateKeySet(keySet),
|
||||
|
155
internal/server/authorization.go
Normal file
155
internal/server/authorization.go
Normal file
@ -0,0 +1,155 @@
|
||||
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 {
|
||||
logger.Error(ctx, "could not retrieve user", logger.E(errors.WithStack(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.Error(
|
||||
ctx, "unexpected user type",
|
||||
logger.F("subject", user.Subject()),
|
||||
logger.F("type", fmt.Sprintf("%T", user)),
|
||||
)
|
||||
}
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"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/auth/thirdparty"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/config"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
@ -105,19 +105,19 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
user.NewAuthenticator(keys, string(s.conf.Issuer)),
|
||||
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer)),
|
||||
agent.NewAuthenticator(s.agentRepo),
|
||||
))
|
||||
|
||||
r.Route("/agents", func(r chi.Router) {
|
||||
r.Get("/", s.queryAgents)
|
||||
r.Get("/{agentID}", s.getAgent)
|
||||
r.Put("/{agentID}", s.updateAgent)
|
||||
r.Delete("/{agentID}", s.deleteAgent)
|
||||
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.Get("/{agentID}/specs", s.getAgentSpecs)
|
||||
r.Post("/{agentID}/specs", s.updateSpec)
|
||||
r.Delete("/{agentID}/specs", s.deleteSpec)
|
||||
r.With(assertAgentReadAccess).Get("/{agentID}/specs", s.getAgentSpecs)
|
||||
r.With(assertAgentWriteAccess).Post("/{agentID}/specs", s.updateSpec)
|
||||
r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", s.deleteSpec)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -33,7 +33,9 @@ func (s *Spec) SpecData() map[string]any {
|
||||
}
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{}
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
}
|
||||
}
|
||||
|
||||
var _ spec.Spec = &Spec{}
|
||||
|
28
internal/spec/compare.go
Normal file
28
internal/spec/compare.go
Normal file
@ -0,0 +1,28 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func Equals(a Spec, b Spec) (bool, error) {
|
||||
if a.SpecName() != b.SpecName() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if a.SpecRevision() != b.SpecRevision() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
hashA, err := hashstructure.Hash(a.SpecData(), hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
hashB, err := hashstructure.Hash(b.SpecData(), hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return hashA == hashB, nil
|
||||
}
|
@ -32,6 +32,7 @@ func (s *Spec) SpecData() map[string]any {
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
Gateways: make(map[ID]GatewayEntry),
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ func (s *Spec) SpecData() map[string]any {
|
||||
|
||||
func NewSpec() *Spec {
|
||||
return &Spec{
|
||||
Revision: -1,
|
||||
PostImportCommands: make([]*UCIPostImportCommand, 0),
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user