feat: authenticate users and agents requests

This commit is contained in:
2023-03-07 23:10:42 +01:00
parent bd0d5a621a
commit ee69644699
31 changed files with 590 additions and 125 deletions

View File

@ -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,

View 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{}

View 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
}

View 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{}

View File

@ -0,0 +1,81 @@
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 = context.WithValue(ctx, contextKeyUser, user)
h.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
}

View 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{}

53
internal/auth/user/jwt.go Normal file
View File

@ -0,0 +1,53 @@
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, role Role) (string, error) {
token := jwt.New()
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
}

View 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{}

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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)
}

View 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
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,13 @@
package auth
import (
"github.com/urfave/cli/v2"
)
func Root() *cli.Command {
return &cli.Command{
Name: "auth",
Usage: "Authentication related commands",
Subcommands: []*cli.Command{},
}
}

View File

@ -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 {

View File

@ -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
},

View File

@ -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 {

View File

@ -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(),
},
}
}

View File

@ -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)

View File

@ -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
View 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(),
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
})
})
})

View File

@ -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