feat: authenticate users and agents requests
This commit is contained in:
@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/client"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
@ -15,7 +16,7 @@ import (
|
||||
type Agent struct {
|
||||
thumbprint string
|
||||
privateKey jwk.Key
|
||||
client *client.Client
|
||||
serverURL string
|
||||
controllers []Controller
|
||||
interval time.Duration
|
||||
collectors []metadata.Collector
|
||||
@ -29,7 +30,15 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(a.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
ctx = withClient(ctx, a.client)
|
||||
logger.Info(ctx, "generating token")
|
||||
token, err := agent.GenerateToken(a.privateKey, a.thumbprint)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
client := client.New(a.serverURL, client.WithToken(token))
|
||||
|
||||
ctx = withClient(ctx, client)
|
||||
|
||||
for {
|
||||
select {
|
||||
@ -37,7 +46,7 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
|
||||
logger.Debug(ctx, "registering agent")
|
||||
|
||||
if err := a.registerAgent(ctx, state); err != nil {
|
||||
if err := a.registerAgent(ctx, client, state); err != nil {
|
||||
logger.Error(ctx, "could not register agent", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
@ -76,7 +85,7 @@ func (a *Agent) Reconcile(ctx context.Context, state *State) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) registerAgent(ctx context.Context, state *State) error {
|
||||
func (a *Agent) registerAgent(ctx context.Context, client *client.Client, state *State) error {
|
||||
meta, err := a.collectMetadata(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -84,7 +93,7 @@ func (a *Agent) registerAgent(ctx context.Context, state *State) error {
|
||||
|
||||
sorted := metadata.Sort(meta)
|
||||
|
||||
agent, err := a.client.RegisterAgent(ctx, a.privateKey, a.thumbprint, sorted)
|
||||
agent, err := client.RegisterAgent(ctx, a.privateKey, a.thumbprint, sorted)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -129,12 +138,10 @@ func New(serverURL string, privateKey jwk.Key, thumbprint string, funcs ...Optio
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
client := client.New(serverURL)
|
||||
|
||||
return &Agent{
|
||||
serverURL: serverURL,
|
||||
privateKey: privateKey,
|
||||
thumbprint: thumbprint,
|
||||
client: client,
|
||||
controllers: opt.Controllers,
|
||||
interval: opt.Interval,
|
||||
collectors: opt.Collectors,
|
||||
|
92
internal/auth/agent/authenticator.go
Normal file
92
internal/auth/agent/authenticator.go
Normal file
@ -0,0 +1,92 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
repo datastore.AgentRepository
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth.User, error) {
|
||||
ctx = logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr))
|
||||
|
||||
authorization := r.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
rawToken := strings.TrimPrefix(authorization, "Bearer ")
|
||||
if rawToken == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
token, err := jwt.Parse([]byte(rawToken), jwt.WithVerify(false))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawThumbprint, exists := token.Get(keyThumbprint)
|
||||
if !exists {
|
||||
return nil, errors.Errorf("could not find '%s' claim", keyThumbprint)
|
||||
}
|
||||
|
||||
thumbrint, ok := rawThumbprint.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected '%s' claim value: '%v'", keyThumbprint, rawThumbprint)
|
||||
}
|
||||
|
||||
agents, _, err := a.repo.Query(
|
||||
ctx,
|
||||
datastore.WithAgentQueryThumbprints(thumbrint),
|
||||
datastore.WithAgentQueryLimit(1),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(agents) != 1 {
|
||||
return nil, errors.Errorf("unexpected number of found agents: '%d'", len(agents))
|
||||
}
|
||||
|
||||
agent, err := a.repo.Get(
|
||||
ctx,
|
||||
agents[0].ID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err = jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
|
||||
jwt.WithValidate(true),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
agent: agent,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(repo datastore.AgentRepository) *Authenticator {
|
||||
return &Authenticator{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
var _ auth.Authenticator = &Authenticator{}
|
37
internal/auth/agent/jwt.go
Normal file
37
internal/auth/agent/jwt.go
Normal file
@ -0,0 +1,37 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const keyThumbprint = "thumbprint"
|
||||
|
||||
func GenerateToken(key jwk.Key, thumbprint string) (string, error) {
|
||||
token := jwt.New()
|
||||
|
||||
if err := token.Set(keyThumbprint, thumbprint); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(jwt.IssuedAtKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, key))
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return string(rawToken), nil
|
||||
}
|
23
internal/auth/agent/user.go
Normal file
23
internal/auth/agent/user.go
Normal file
@ -0,0 +1,23 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
agent *datastore.Agent
|
||||
}
|
||||
|
||||
// Subject implements auth.User
|
||||
func (u *User) Subject() string {
|
||||
return fmt.Sprintf("agent-%d", u.agent.ID)
|
||||
}
|
||||
|
||||
func (u *User) Agent() *datastore.Agent {
|
||||
return u.agent
|
||||
}
|
||||
|
||||
var _ auth.User = &User{}
|
82
internal/auth/middleware.go
Normal file
82
internal/auth/middleware.go
Normal file
@ -0,0 +1,82 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeUnauthorized api.ErrorCode = "unauthorized"
|
||||
ErrCodeForbidden api.ErrorCode = "forbidden"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
contextKeyUser contextKey = "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))
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = errors.New("unauthenticated")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
)
|
||||
|
||||
type User interface {
|
||||
Subject() string
|
||||
}
|
||||
|
||||
type Authenticator interface {
|
||||
Authenticate(context.Context, *http.Request) (User, error)
|
||||
}
|
||||
|
||||
func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr))
|
||||
|
||||
var (
|
||||
user User
|
||||
err error
|
||||
)
|
||||
|
||||
for _, auth := range authenticators {
|
||||
user, err = auth.Authenticate(ctx, r)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "could not authenticate request", logger.E(errors.WithStack(err)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
api.ErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("user", user.Subject()))
|
||||
ctx = context.WithValue(ctx, contextKeyUser, user)
|
||||
|
||||
h.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
67
internal/auth/user/authenticator.go
Normal file
67
internal/auth/user/authenticator.go
Normal file
@ -0,0 +1,67 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
keys jwk.Set
|
||||
issuer string
|
||||
}
|
||||
|
||||
// Authenticate implements auth.Authenticator.
|
||||
func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth.User, error) {
|
||||
ctx = logger.With(r.Context(), logger.F("remoteAddr", r.RemoteAddr))
|
||||
|
||||
authorization := r.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
rawToken := strings.TrimPrefix(authorization, "Bearer ")
|
||||
if rawToken == "" {
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
token, err := parseToken(ctx, a.keys, a.issuer, rawToken)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawRole, exists := token.Get(keyRole)
|
||||
if !exists {
|
||||
return nil, errors.New("could not find 'thumbprint' claim")
|
||||
}
|
||||
|
||||
role, ok := rawRole.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected '%s' claim value: '%v'", keyRole, rawRole)
|
||||
}
|
||||
|
||||
if !isValidRole(role) {
|
||||
return nil, errors.Errorf("invalid role '%s'", role)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
subject: token.Subject(),
|
||||
role: Role(role),
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(keys jwk.Set, issuer string) *Authenticator {
|
||||
return &Authenticator{
|
||||
keys: keys,
|
||||
issuer: issuer,
|
||||
}
|
||||
}
|
||||
|
||||
var _ auth.Authenticator = &Authenticator{}
|
61
internal/auth/user/jwt.go
Normal file
61
internal/auth/user/jwt.go
Normal file
@ -0,0 +1,61 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const keyRole = "role"
|
||||
|
||||
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string) (jwt.Token, error) {
|
||||
token, err := jwt.Parse(
|
||||
[]byte(rawToken),
|
||||
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
|
||||
jwt.WithIssuer(issuer),
|
||||
jwt.WithValidate(true),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, role Role) (string, error) {
|
||||
token := jwt.New()
|
||||
|
||||
if err := token.Set(jwt.SubjectKey, subject); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(jwt.IssuerKey, issuer); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(keyRole, role); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(jwt.IssuedAtKey, now); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, key))
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return string(rawToken), nil
|
||||
}
|
32
internal/auth/user/user.go
Normal file
32
internal/auth/user/user.go
Normal file
@ -0,0 +1,32 @@
|
||||
package user
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleWriter Role = "writer"
|
||||
RoleReader Role = "reader"
|
||||
)
|
||||
|
||||
func isValidRole(r string) bool {
|
||||
rr := Role(r)
|
||||
|
||||
return rr == RoleWriter || rr == RoleReader
|
||||
}
|
||||
|
||||
type User struct {
|
||||
subject string
|
||||
role Role
|
||||
}
|
||||
|
||||
// Subject implements auth.User
|
||||
func (u *User) Subject() string {
|
||||
return u.subject
|
||||
}
|
||||
|
||||
func (u *User) Role() Role {
|
||||
return u.role
|
||||
}
|
||||
|
||||
var _ auth.User = &User{}
|
@ -13,44 +13,49 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
http *http.Client
|
||||
token string
|
||||
serverURL string
|
||||
http *http.Client
|
||||
defaultOpts Options
|
||||
serverURL string
|
||||
}
|
||||
|
||||
func (c *Client) apiGet(ctx context.Context, path string, result any) error {
|
||||
if err := c.apiDo(ctx, http.MethodGet, path, nil, result); err != nil {
|
||||
func (c *Client) apiGet(ctx context.Context, path string, result any, funcs ...OptionFunc) error {
|
||||
if err := c.apiDo(ctx, http.MethodGet, path, nil, result, funcs...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) apiPost(ctx context.Context, path string, payload any, result any) error {
|
||||
if err := c.apiDo(ctx, http.MethodPost, path, payload, result); err != nil {
|
||||
func (c *Client) apiPost(ctx context.Context, path string, payload any, result any, funcs ...OptionFunc) error {
|
||||
if err := c.apiDo(ctx, http.MethodPost, path, payload, result, funcs...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) apiPut(ctx context.Context, path string, payload any, result any) error {
|
||||
if err := c.apiDo(ctx, http.MethodPut, path, payload, result); err != nil {
|
||||
func (c *Client) apiPut(ctx context.Context, path string, payload any, result any, funcs ...OptionFunc) error {
|
||||
if err := c.apiDo(ctx, http.MethodPut, path, payload, result, funcs...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) apiDelete(ctx context.Context, path string, payload any, result any) error {
|
||||
if err := c.apiDo(ctx, http.MethodDelete, path, payload, result); err != nil {
|
||||
func (c *Client) apiDelete(ctx context.Context, path string, payload any, result any, funcs ...OptionFunc) error {
|
||||
if err := c.apiDo(ctx, http.MethodDelete, path, payload, result, funcs...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) apiDo(ctx context.Context, method string, path string, payload any, response any) error {
|
||||
func (c *Client) apiDo(ctx context.Context, method string, path string, payload any, response any, funcs ...OptionFunc) error {
|
||||
opts := c.defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
url := c.serverURL + path
|
||||
|
||||
logger.Debug(
|
||||
@ -73,6 +78,12 @@ func (c *Client) apiDo(ctx context.Context, method string, path string, payload
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for key, values := range opts.Headers {
|
||||
for _, v := range values {
|
||||
req.Header.Add(key, v)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -89,6 +100,12 @@ func (c *Client) apiDo(ctx context.Context, method string, path string, payload
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) defaultOptions() *Options {
|
||||
return &Options{
|
||||
Headers: c.defaultOpts.Headers,
|
||||
}
|
||||
}
|
||||
|
||||
func withResponse[T any]() struct {
|
||||
Data T
|
||||
Error *api.Error
|
||||
@ -113,9 +130,15 @@ func joinSlice[T any](items []T) string {
|
||||
return str
|
||||
}
|
||||
|
||||
func New(serverURL string) *Client {
|
||||
func New(serverURL string, funcs ...OptionFunc) *Client {
|
||||
opts := Options{}
|
||||
for _, fn := range funcs {
|
||||
fn(&opts)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
serverURL: serverURL,
|
||||
http: &http.Client{},
|
||||
serverURL: serverURL,
|
||||
http: &http.Client{},
|
||||
defaultOpts: opts,
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,14 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) GetAgent(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||
func (c *Client) GetAgent(ctx context.Context, agentID datastore.AgentID, funcs ...OptionFunc) (*datastore.Agent, error) {
|
||||
response := withResponse[struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}]()
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d", agentID)
|
||||
|
||||
if err := c.apiGet(ctx, path, &response); err != nil {
|
||||
if err := c.apiGet(ctx, path, &response, funcs...); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
@ -9,14 +9,14 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) GetAgentSpecs(ctx context.Context, agentID datastore.AgentID) ([]spec.Spec, error) {
|
||||
func (c *Client) GetAgentSpecs(ctx context.Context, agentID datastore.AgentID, funcs ...OptionFunc) ([]spec.Spec, error) {
|
||||
response := withResponse[struct {
|
||||
Specs []*spec.RawSpec `json:"specs"`
|
||||
}]()
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
||||
|
||||
if err := c.apiGet(ctx, path, &response); err != nil {
|
||||
if err := c.apiGet(ctx, path, &response, funcs...); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
24
internal/client/options.go
Normal file
24
internal/client/options.go
Normal file
@ -0,0 +1,24 @@
|
||||
package client
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Options struct {
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
type OptionFunc func(*Options)
|
||||
|
||||
func WithToken(token string) OptionFunc {
|
||||
return func(o *Options) {
|
||||
if o.Headers == nil {
|
||||
o.Headers = http.Header{}
|
||||
}
|
||||
o.Headers.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
}
|
||||
|
||||
func WithHeaders(headers http.Header) OptionFunc {
|
||||
return func(o *Options) {
|
||||
o.Headers = headers
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
type QueryAgentsOptionFunc func(*QueryAgentsOptions)
|
||||
|
||||
type QueryAgentsOptions struct {
|
||||
Options []OptionFunc
|
||||
Limit *int
|
||||
Offset *int
|
||||
Thumbprints []string
|
||||
@ -19,6 +20,12 @@ type QueryAgentsOptions struct {
|
||||
Statuses []datastore.AgentStatus
|
||||
}
|
||||
|
||||
func WithQueryAgentsOptions(funcs ...OptionFunc) QueryAgentsOptionFunc {
|
||||
return func(opts *QueryAgentsOptions) {
|
||||
opts.Options = funcs
|
||||
}
|
||||
}
|
||||
|
||||
func WithQueryAgentsLimit(limit int) QueryAgentsOptionFunc {
|
||||
return func(opts *QueryAgentsOptions) {
|
||||
opts.Limit = &limit
|
||||
@ -76,7 +83,11 @@ func (c *Client) QueryAgents(ctx context.Context, funcs ...QueryAgentsOptionFunc
|
||||
Total int `json:"total"`
|
||||
}]()
|
||||
|
||||
if err := c.apiGet(ctx, path, &response); err != nil {
|
||||
if options.Options == nil {
|
||||
options.Options = make([]OptionFunc, 0)
|
||||
}
|
||||
|
||||
if err := c.apiGet(ctx, path, &response, options.Options...); err != nil {
|
||||
return nil, 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) RegisterAgent(ctx context.Context, key jwk.Key, thumbprint string, meta []metadata.Tuple) (*datastore.Agent, error) {
|
||||
func (c *Client) RegisterAgent(ctx context.Context, key jwk.Key, thumbprint string, meta []metadata.Tuple, funcs ...OptionFunc) (*datastore.Agent, error) {
|
||||
keySet, err := jwk.PublicKeySet(key)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
@ -36,7 +36,7 @@ func (c *Client) RegisterAgent(ctx context.Context, key jwk.Key, thumbprint stri
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}]()
|
||||
|
||||
if err := c.apiPost(ctx, "/api/v1/register", payload, &response); err != nil {
|
||||
if err := c.apiPost(ctx, "/api/v1/register", payload, &response, funcs...); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,8 @@ import (
|
||||
)
|
||||
|
||||
type UpdateAgentOptions struct {
|
||||
Status *int
|
||||
Status *int
|
||||
Options []OptionFunc
|
||||
}
|
||||
|
||||
type UpdateAgentOptionFunc func(*UpdateAgentOptions)
|
||||
@ -20,6 +21,12 @@ func WithAgentStatus(status int) UpdateAgentOptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc {
|
||||
return func(opts *UpdateAgentOptions) {
|
||||
opts.Options = funcs
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, funcs ...UpdateAgentOptionFunc) (*datastore.Agent, error) {
|
||||
opts := &UpdateAgentOptions{}
|
||||
for _, fn := range funcs {
|
||||
@ -38,7 +45,11 @@ func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, fun
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d", agentID)
|
||||
|
||||
if err := c.apiPut(ctx, path, payload, &response); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) UpdateAgentSpec(ctx context.Context, agentID datastore.AgentID, spc spec.Spec) (*datastore.Spec, error) {
|
||||
func (c *Client) UpdateAgentSpec(ctx context.Context, agentID datastore.AgentID, spc spec.Spec, funcs ...OptionFunc) (*datastore.Spec, error) {
|
||||
payload := struct {
|
||||
Name spec.Name `json:"name"`
|
||||
Revision int `json:"revision"`
|
||||
@ -27,7 +27,7 @@ func (c *Client) UpdateAgentSpec(ctx context.Context, agentID datastore.AgentID,
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
||||
|
||||
if err := c.apiPost(ctx, path, payload, &response); err != nil {
|
||||
if err := c.apiPost(ctx, path, payload, &response, funcs...); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/machineid"
|
||||
"github.com/pkg/errors"
|
||||
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
@ -20,12 +20,17 @@ func GetCommand() *cli.Command {
|
||||
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 := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
agent, err := client.GetAgent(ctx.Context, agentID)
|
||||
if err != nil {
|
||||
|
@ -18,7 +18,13 @@ func QueryCommand() *cli.Command {
|
||||
Flags: clientFlag.ComposeFlags(),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
client := client.New(baseFlags.ServerURL)
|
||||
|
||||
token, err := clientFlag.GetToken(baseFlags)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
agents, _, err := client.QueryAgents(ctx.Context)
|
||||
if err != nil {
|
||||
|
@ -26,6 +26,11 @@ func UpdateCommand() *cli.Command {
|
||||
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)
|
||||
@ -38,7 +43,7 @@ func UpdateCommand() *cli.Command {
|
||||
options = append(options, client.WithAgentStatus(status))
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL)
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)
|
||||
if err != nil {
|
||||
|
@ -2,9 +2,13 @@ package flag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format/table"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
@ -28,6 +32,17 @@ func ComposeFlags(flags ...cli.Flag) []cli.Flag {
|
||||
Usage: fmt.Sprintf("use `MODE` as output mode (available: %s)", []format.OutputMode{format.OutputModeCompact, format.OutputModeWide}),
|
||||
Value: string(format.OutputModeCompact),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "use `TOKEN` as authentification token",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token-file",
|
||||
Usage: "use `TOKEN_FILE` as file containing the authentification token",
|
||||
Value: ".emissary-token",
|
||||
TakesFile: true,
|
||||
},
|
||||
}
|
||||
|
||||
flags = append(flags, baseFlags...)
|
||||
@ -39,16 +54,43 @@ type BaseFlags struct {
|
||||
ServerURL string
|
||||
Format format.Format
|
||||
OutputMode format.OutputMode
|
||||
Token string
|
||||
TokenFile string
|
||||
}
|
||||
|
||||
func GetBaseFlags(ctx *cli.Context) *BaseFlags {
|
||||
serverURL := ctx.String("server")
|
||||
rawFormat := ctx.String("format")
|
||||
rawOutputMode := ctx.String("output-mode")
|
||||
tokenFile := ctx.String("token-file")
|
||||
token := ctx.String("token")
|
||||
|
||||
return &BaseFlags{
|
||||
ServerURL: serverURL,
|
||||
Format: format.Format(rawFormat),
|
||||
OutputMode: format.OutputMode(rawOutputMode),
|
||||
Token: token,
|
||||
TokenFile: tokenFile,
|
||||
}
|
||||
}
|
||||
|
||||
func GetToken(flags *BaseFlags) (string, error) {
|
||||
if flags.Token != "" {
|
||||
return flags.Token, nil
|
||||
}
|
||||
|
||||
if flags.TokenFile == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
rawToken, err := ioutil.ReadFile(flags.TokenFile)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if rawToken == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(rawToken)), nil
|
||||
}
|
||||
|
54
internal/command/server/auth/create_token.go
Normal file
54
internal/command/server/auth/create_token.go
Normal file
@ -0,0 +1,54 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func CreateTokenCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create-token",
|
||||
Usage: "Create a new authentification 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),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "subject",
|
||||
Usage: "associate `SUBJECT` to the token",
|
||||
Value: fmt.Sprintf("user-%s", shortuuid.New()),
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
conf, err := common.LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
}
|
||||
|
||||
subject := ctx.String("subject")
|
||||
role := ctx.String("role")
|
||||
|
||||
key, err := jwk.LoadOrGenerate(string(conf.Server.PrivateKeyPath), jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := user.GenerateToken(ctx.Context, key, string(conf.Server.Issuer), subject, user.Role(role))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
fmt.Println(token)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
15
internal/command/server/auth/root.go
Normal file
15
internal/command/server/auth/root.go
Normal file
@ -0,0 +1,15 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func Root() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "auth",
|
||||
Usage: "Authentication related commands",
|
||||
Subcommands: []*cli.Command{
|
||||
CreateTokenCommand(),
|
||||
},
|
||||
}
|
||||
}
|
@ -36,8 +36,8 @@ func MigrateCommand() *cli.Command {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
}
|
||||
|
||||
driver := string(conf.Database.Driver)
|
||||
dsn := string(conf.Database.DSN)
|
||||
driver := string(conf.Server.Database.Driver)
|
||||
dsn := string(conf.Server.Database.DSN)
|
||||
|
||||
migr, err := migrate.New("migrations", driver, dsn)
|
||||
if err != nil {
|
||||
|
@ -20,10 +20,10 @@ func PingCommand() *cli.Command {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
}
|
||||
|
||||
logger.Info(ctx.Context, "connecting to database", logger.F("dsn", conf.Database.DSN))
|
||||
logger.Info(ctx.Context, "connecting to database", logger.F("dsn", conf.Server.Database.DSN))
|
||||
|
||||
driver := string(conf.Database.Driver)
|
||||
dsn := string(conf.Database.DSN)
|
||||
driver := string(conf.Server.Database.Driver)
|
||||
dsn := string(conf.Server.Database.DSN)
|
||||
|
||||
db, err := sql.Open(driver, dsn)
|
||||
if err != nil {
|
||||
@ -40,7 +40,7 @@ func PingCommand() *cli.Command {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx.Context, "connection succeeded", logger.F("dsn", conf.Database.DSN))
|
||||
logger.Info(ctx.Context, "connection succeeded", logger.F("dsn", conf.Server.Database.DSN))
|
||||
|
||||
return nil
|
||||
},
|
||||
|
@ -18,8 +18,8 @@ func ResetCommand() *cli.Command {
|
||||
return errors.Wrap(err, "Could not load configuration")
|
||||
}
|
||||
|
||||
driver := string(conf.Database.Driver)
|
||||
dsn := string(conf.Database.DSN)
|
||||
driver := string(conf.Server.Database.Driver)
|
||||
dsn := string(conf.Server.Database.DSN)
|
||||
|
||||
migr, err := migrate.New("migrations", driver, dsn)
|
||||
if err != nil {
|
||||
|
@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/config"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/server/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/server/database"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -14,6 +15,7 @@ func Root() *cli.Command {
|
||||
RunCommand(),
|
||||
database.Root(),
|
||||
config.Root(),
|
||||
auth.Root(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func RunCommand() *cli.Command {
|
||||
logger.SetLevel(logger.Level(conf.Logger.Level))
|
||||
|
||||
srv := server.New(
|
||||
server.WithConfig(conf),
|
||||
server.WithConfig(conf.Server),
|
||||
)
|
||||
|
||||
addrs, srvErrs := srv.Start(ctx.Context)
|
||||
|
@ -10,11 +10,9 @@ import (
|
||||
|
||||
// Config definition
|
||||
type Config struct {
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
Logger LoggerConfig `yaml:"logger"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
Agent AgentConfig `yaml:"agent"`
|
||||
Logger LoggerConfig `yaml:"logger"`
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Agent AgentConfig `yaml:"agent"`
|
||||
}
|
||||
|
||||
// NewFromFile retrieves the configuration from the given file
|
||||
@ -43,11 +41,9 @@ func NewDumpDefault() *Config {
|
||||
// NewDefault return new default configuration
|
||||
func NewDefault() *Config {
|
||||
return &Config{
|
||||
HTTP: NewDefaultHTTPConfig(),
|
||||
Logger: NewDefaultLoggerConfig(),
|
||||
Database: NewDefaultDatabaseConfig(),
|
||||
CORS: NewDefaultCORSConfig(),
|
||||
Agent: NewDefaultAgentConfig(),
|
||||
Logger: NewDefaultLoggerConfig(),
|
||||
Agent: NewDefaultAgentConfig(),
|
||||
Server: NewDefaultServerConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
19
internal/config/server.go
Normal file
19
internal/config/server.go
Normal file
@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
type ServerConfig struct {
|
||||
PrivateKeyPath InterpolatedString `yaml:"privateKeyPath"`
|
||||
Issuer InterpolatedString `yaml:"issuer"`
|
||||
HTTP HTTPConfig `yaml:"http"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
}
|
||||
|
||||
func NewDefaultServerConfig() ServerConfig {
|
||||
return ServerConfig{
|
||||
PrivateKeyPath: "server-key.json",
|
||||
Issuer: "http://127.0.0.1:3000",
|
||||
HTTP: NewDefaultHTTPConfig(),
|
||||
Database: NewDefaultDatabaseConfig(),
|
||||
CORS: NewDefaultCORSConfig(),
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) initRepositories(ctx context.Context) error {
|
||||
agentRepo, err := setup.NewAgentRepository(ctx, s.conf)
|
||||
agentRepo, err := setup.NewAgentRepository(ctx, s.conf.Database)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -1,20 +1,22 @@
|
||||
package server
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/config"
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/config"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Config *config.Config
|
||||
Config config.ServerConfig
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
|
||||
func defaultOption() *Option {
|
||||
return &Option{
|
||||
Config: config.NewDefault(),
|
||||
Config: config.NewDefaultServerConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func WithConfig(conf *config.Config) OptionFunc {
|
||||
func WithConfig(conf config.ServerConfig) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Config = conf
|
||||
}
|
||||
|
@ -7,8 +7,12 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/user"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/config"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
@ -17,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
conf *config.Config
|
||||
conf config.ServerConfig
|
||||
agentRepo datastore.AgentRepository
|
||||
}
|
||||
|
||||
@ -68,6 +72,20 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
}
|
||||
}()
|
||||
|
||||
key, err := jwk.LoadOrGenerate(string(s.conf.PrivateKeyPath), jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := jwk.PublicKeySet(key)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.Logger)
|
||||
@ -85,15 +103,22 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
router.Route("/api/v1", func(r chi.Router) {
|
||||
r.Post("/register", s.registerAgent)
|
||||
|
||||
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.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
user.NewAuthenticator(keys, string(s.conf.Issuer)),
|
||||
agent.NewAuthenticator(s.agentRepo),
|
||||
))
|
||||
|
||||
r.Get("/{agentID}/specs", s.getAgentSpecs)
|
||||
r.Post("/{agentID}/specs", s.updateSpec)
|
||||
r.Delete("/{agentID}/specs", s.deleteSpec)
|
||||
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.Get("/{agentID}/specs", s.getAgentSpecs)
|
||||
r.Post("/{agentID}/specs", s.updateSpec)
|
||||
r.Delete("/{agentID}/specs", s.deleteSpec)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -31,9 +31,9 @@ func openPostgresPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
|
||||
return postgresPool, nil
|
||||
}
|
||||
|
||||
func NewAgentRepository(ctx context.Context, conf *config.Config) (datastore.AgentRepository, error) {
|
||||
driver := string(conf.Database.Driver)
|
||||
dsn := string(conf.Database.DSN)
|
||||
func NewAgentRepository(ctx context.Context, conf config.DatabaseConfig) (datastore.AgentRepository, error) {
|
||||
driver := string(conf.Driver)
|
||||
dsn := string(conf.DSN)
|
||||
|
||||
var agentRepository datastore.AgentRepository
|
||||
|
||||
|
Reference in New Issue
Block a user