feat: refactor api mount
This commit is contained in:
parent
ca4211daef
commit
15a0bf6ecc
@ -48,7 +48,7 @@ func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantI
|
||||
now := time.Now().UTC()
|
||||
|
||||
query = `
|
||||
UPDATE agents SET tenant_id = $1, updated_at = $2
|
||||
UPDATE agents SET tenant_id = $1, updated_at = $2 WHERE id = $3
|
||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||
`
|
||||
|
||||
@ -56,6 +56,7 @@ func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantI
|
||||
ctx, query,
|
||||
tenantID,
|
||||
now,
|
||||
agentID,
|
||||
)
|
||||
|
||||
metadata := JSONMap{}
|
||||
@ -85,8 +86,47 @@ func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantI
|
||||
}
|
||||
|
||||
// Detach implements datastore.AgentRepository.
|
||||
func (*AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||
panic("unimplemented")
|
||||
func (r *AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||
var agent datastore.Agent
|
||||
|
||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
query := `
|
||||
UPDATE agents SET tenant_id = null, updated_at = $1 WHERE id = $2
|
||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||
`
|
||||
|
||||
row := tx.QueryRowContext(
|
||||
ctx, query,
|
||||
now,
|
||||
agentID,
|
||||
)
|
||||
|
||||
metadata := JSONMap{}
|
||||
var rawKeySet []byte
|
||||
|
||||
err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
|
||||
keySet, err := jwk.Parse(rawKeySet)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
agent.KeySet = &datastore.SerializableKeySet{keySet}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &agent, nil
|
||||
}
|
||||
|
||||
// DeleteSpec implements datastore.AgentRepository.
|
||||
|
@ -1,504 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
||||
ErrCodeNotFound api.ErrorCode = "not-found"
|
||||
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
||||
ErrCodeConflict api.ErrorCode = "conflict"
|
||||
ErrCodeMultipleResults api.ErrorCode = "multiple-results"
|
||||
ErrCodeAlreadyClaimed api.ErrorCode = "already-claimed"
|
||||
)
|
||||
|
||||
type registerAgentRequest struct {
|
||||
KeySet json.RawMessage `json:"keySet" validate:"required"`
|
||||
Metadata []metadata.Tuple `json:"metadata" validate:"required"`
|
||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||
Signature string `json:"signature" validate:"required"`
|
||||
}
|
||||
|
||||
func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
registerAgentReq := ®isterAgentRequest{}
|
||||
if ok := api.Bind(w, r, registerAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
keySet, err := jwk.Parse(registerAgentReq.KeySet)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not parse key set", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint))
|
||||
|
||||
// Validate that the existing signature validates the request
|
||||
|
||||
validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not validate signature", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !validSignature {
|
||||
logger.Warn(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature))
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
metadata := metadata.FromSorted(registerAgentReq.Metadata)
|
||||
|
||||
agent, err := s.agentRepo.Create(
|
||||
ctx,
|
||||
registerAgentReq.Thumbprint,
|
||||
keySet,
|
||||
metadata,
|
||||
)
|
||||
if err != nil {
|
||||
if !errors.Is(err, datastore.ErrAlreadyExist) {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not create agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agents, _, err := s.agentRepo.Query(
|
||||
ctx,
|
||||
datastore.WithAgentQueryThumbprints(registerAgentReq.Thumbprint),
|
||||
datastore.WithAgentQueryLimit(1),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not retrieve agents", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(agents) == 0 {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not retrieve matching agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agentID := agents[0].ID
|
||||
|
||||
agent, err = s.agentRepo.Get(ctx, agentID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(
|
||||
ctx, "could not retrieve agent",
|
||||
logger.CapturedE(err), logger.F("agentID", agentID),
|
||||
)
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not validate signature using previous keyset", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent, err = s.agentRepo.Update(
|
||||
ctx,
|
||||
agents[0].ID,
|
||||
datastore.WithAgentUpdateKeySet(keySet),
|
||||
datastore.WithAgentUpdateMetadata(metadata),
|
||||
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusCreated, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
||||
|
||||
type updateAgentRequest struct {
|
||||
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
||||
Label *string `json:"label" validate:"omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
updateAgentReq := &updateAgentRequest{}
|
||||
if ok := api.Bind(w, r, updateAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
options := make([]datastore.AgentUpdateOptionFunc, 0)
|
||||
|
||||
if updateAgentReq.Status != nil {
|
||||
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
||||
}
|
||||
|
||||
if updateAgentReq.Label != nil {
|
||||
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
||||
}
|
||||
|
||||
agent, err := s.agentRepo.Update(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) queryAgents(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
offset, ok := getIntQueryParam(w, r, "offset", 0)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
options := []datastore.AgentQueryOptionFunc{
|
||||
datastore.WithAgentQueryLimit(int(limit)),
|
||||
datastore.WithAgentQueryOffset(int(offset)),
|
||||
datastore.WithAgentQueryTenantID(user.Tenant()),
|
||||
}
|
||||
|
||||
ids, ok := getIntSliceValues(w, r, "ids", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if ids != nil {
|
||||
agentIDs := func(ids []int64) []datastore.AgentID {
|
||||
agentIDs := make([]datastore.AgentID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
agentIDs = append(agentIDs, datastore.AgentID(id))
|
||||
}
|
||||
|
||||
return agentIDs
|
||||
}(ids)
|
||||
|
||||
options = append(options, datastore.WithAgentQueryID(agentIDs...))
|
||||
}
|
||||
|
||||
thumbprints, ok := getStringSliceValues(w, r, "thumbprints", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if thumbprints != nil {
|
||||
options = append(options, datastore.WithAgentQueryThumbprints(thumbprints...))
|
||||
}
|
||||
|
||||
statuses, ok := getIntSliceValues(w, r, "statuses", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if statuses != nil {
|
||||
agentStatuses := func(statuses []int64) []datastore.AgentStatus {
|
||||
agentStatuses := make([]datastore.AgentStatus, 0, len(statuses))
|
||||
for _, status := range statuses {
|
||||
agentStatuses = append(agentStatuses, datastore.AgentStatus(status))
|
||||
}
|
||||
|
||||
return agentStatuses
|
||||
}(statuses)
|
||||
|
||||
options = append(options, datastore.WithAgentQueryStatus(agentStatuses...))
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
agents, total, err := s.agentRepo.Query(
|
||||
ctx,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not list agents", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agents []*datastore.Agent `json:"agents"`
|
||||
Total int `json:"total"`
|
||||
}{
|
||||
Agents: agents,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if ok := s.assertTenantOwns(w, r, agentID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
err := s.agentRepo.Delete(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not delete agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
AgentID datastore.AgentID `json:"agentId"`
|
||||
}{
|
||||
AgentID: datastore.AgentID(agentID),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) getAgent(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if ok := s.assertTenantOwns(w, r, agentID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
agent, err := s.agentRepo.Get(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
||||
|
||||
type claimAgentRequest struct {
|
||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||
}
|
||||
|
||||
func (s *Server) claimAgent(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
claimAgentReq := &claimAgentRequest{}
|
||||
if ok := api.Bind(w, r, claimAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
results, _, err := s.agentRepo.Query(
|
||||
ctx,
|
||||
datastore.WithAgentQueryThumbprints(claimAgentReq.Thumbprint),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not query agents", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) > 1 {
|
||||
logger.Error(ctx, "multiple results for agent thumbprint", logger.F("agentThumbprint", claimAgentReq.Thumbprint))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeMultipleResults, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent := results[0]
|
||||
|
||||
if agent.TenantID != nil {
|
||||
logger.Error(ctx, "agent already claimed", logger.F("agentThumbprint", claimAgentReq.Thumbprint), logger.F("agentID", agent.ID), logger.F("tenantID", agent.TenantID))
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyClaimed, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent, err = s.agentRepo.Attach(ctx, user.Tenant(), agent.ID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not attach agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
||||
|
||||
func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool) {
|
||||
rawAgentID := chi.URLParam(r, "agentID")
|
||||
|
||||
agentID, err := strconv.ParseInt(rawAgentID, 10, 64)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse agent id", logger.CapturedE(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return datastore.AgentID(agentID), true
|
||||
}
|
||||
|
||||
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
if rawValue != "" {
|
||||
value, err := strconv.ParseInt(rawValue, 10, 64)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
||||
|
||||
func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
if rawValue != "" {
|
||||
values := strings.Split(rawValue, ",")
|
||||
|
||||
return values, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
||||
|
||||
func getIntSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []int64) ([]int64, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
|
||||
if rawValue != "" {
|
||||
rawValues := strings.Split(rawValue, ",")
|
||||
values := make([]int64, 0, len(rawValues))
|
||||
|
||||
for _, rv := range rawValues {
|
||||
value, err := strconv.ParseInt(rv, 10, 64)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(r.Context(), "could not parse int slice param", logger.F("param", param), logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
return values, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -142,7 +142,7 @@ func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool)
|
||||
return user, true
|
||||
}
|
||||
|
||||
func (s *Server) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentID datastore.AgentID) bool {
|
||||
func (m *Mount) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentID datastore.AgentID) bool {
|
||||
ctx := r.Context()
|
||||
|
||||
user, ok := assertRequestUser(w, r)
|
||||
@ -150,7 +150,7 @@ func (s *Server) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentI
|
||||
return false
|
||||
}
|
||||
|
||||
agent, err := s.agentRepo.Get(ctx, agentID)
|
||||
agent, err := m.agentRepo.Get(ctx, agentID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
77
internal/server/api/claim_agent.go
Normal file
77
internal/server/api/claim_agent.go
Normal file
@ -0,0 +1,77 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type claimAgentRequest struct {
|
||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||
}
|
||||
|
||||
func (m *Mount) claimAgent(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
claimAgentReq := &claimAgentRequest{}
|
||||
if ok := api.Bind(w, r, claimAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
results, _, err := m.agentRepo.Query(
|
||||
ctx,
|
||||
datastore.WithAgentQueryThumbprints(claimAgentReq.Thumbprint),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not query agents", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) > 1 {
|
||||
logger.Error(ctx, "multiple results for agent thumbprint", logger.F("agentThumbprint", claimAgentReq.Thumbprint))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeMultipleResults, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent := results[0]
|
||||
|
||||
if agent.TenantID != nil {
|
||||
logger.Error(ctx, "agent already claimed", logger.F("agentThumbprint", claimAgentReq.Thumbprint), logger.F("agentID", agent.ID), logger.F("tenantID", agent.TenantID))
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyClaimed, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent, err = m.agentRepo.Attach(ctx, user.Tenant(), agent.ID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not attach agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
47
internal/server/api/delete_agent.go
Normal file
47
internal/server/api/delete_agent.go
Normal file
@ -0,0 +1,47 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (m *Mount) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
err := m.agentRepo.Delete(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not delete agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
AgentID datastore.AgentID `json:"agentId"`
|
||||
}{
|
||||
AgentID: datastore.AgentID(agentID),
|
||||
})
|
||||
}
|
47
internal/server/api/get_agent.go
Normal file
47
internal/server/api/get_agent.go
Normal file
@ -0,0 +1,47 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (m *Mount) getAgent(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
agent, err := m.agentRepo.Get(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
85
internal/server/api/get_specs.go
Normal file
85
internal/server/api/get_specs.go
Normal file
@ -0,0 +1,85 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (m *Mount) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
specs, err := m.agentRepo.GetSpecs(ctx, agentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not list specs", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Specs []*datastore.Spec `json:"specs"`
|
||||
}{
|
||||
Specs: specs,
|
||||
})
|
||||
}
|
||||
|
||||
type deleteSpecRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
deleteSpecReq := &deleteSpecRequest{}
|
||||
if ok := api.Bind(w, r, deleteSpecReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
err := m.agentRepo.DeleteSpec(
|
||||
ctx,
|
||||
agentID,
|
||||
deleteSpecReq.Name,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not delete spec", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
Name: deleteSpecReq.Name,
|
||||
})
|
||||
}
|
107
internal/server/api/helper.go
Normal file
107
internal/server/api/helper.go
Normal file
@ -0,0 +1,107 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
||||
ErrCodeNotFound api.ErrorCode = "not-found"
|
||||
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
||||
ErrCodeConflict api.ErrorCode = "conflict"
|
||||
ErrCodeMultipleResults api.ErrorCode = "multiple-results"
|
||||
ErrCodeAlreadyClaimed api.ErrorCode = "already-claimed"
|
||||
)
|
||||
|
||||
func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool) {
|
||||
rawAgentID := chi.URLParam(r, "agentID")
|
||||
|
||||
agentID, err := strconv.ParseInt(rawAgentID, 10, 64)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse agent id", logger.CapturedE(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return datastore.AgentID(agentID), true
|
||||
}
|
||||
|
||||
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
||||
rawSpecID := chi.URLParam(r, "")
|
||||
|
||||
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(r.Context(), "could not parse spec id", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return datastore.SpecID(specID), true
|
||||
}
|
||||
|
||||
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
if rawValue != "" {
|
||||
value, err := strconv.ParseInt(rawValue, 10, 64)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
||||
|
||||
func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
if rawValue != "" {
|
||||
values := strings.Split(rawValue, ",")
|
||||
|
||||
return values, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
||||
|
||||
func getIntSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []int64) ([]int64, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
|
||||
if rawValue != "" {
|
||||
rawValues := strings.Split(rawValue, ",")
|
||||
values := make([]int64, 0, len(rawValues))
|
||||
|
||||
for _, rv := range rawValues {
|
||||
value, err := strconv.ParseInt(rv, 10, 64)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(r.Context(), "could not parse int slice param", logger.F("param", param), logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
return values, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
47
internal/server/api/mount.go
Normal file
47
internal/server/api/mount.go
Normal file
@ -0,0 +1,47 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
)
|
||||
|
||||
type Mount struct {
|
||||
agentRepo datastore.AgentRepository
|
||||
authenticators []auth.Authenticator
|
||||
}
|
||||
|
||||
func (m *Mount) Mount(r chi.Router) {
|
||||
r.NotFound(m.notFound)
|
||||
|
||||
r.Post("/register", m.registerAgent)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(m.authenticators...))
|
||||
|
||||
r.Route("/agents", func(r chi.Router) {
|
||||
r.Post("/claim", m.claimAgent)
|
||||
|
||||
r.With(assertGlobalReadAccess).Get("/", m.queryAgents)
|
||||
|
||||
r.With(assertAgentReadAccess).Get("/{agentID}", m.getAgent)
|
||||
r.With(assertAgentWriteAccess).Put("/{agentID}", m.updateAgent)
|
||||
r.With(assertAgentWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
||||
|
||||
r.With(assertAgentReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
||||
r.With(assertAgentWriteAccess).Post("/{agentID}/specs", m.updateSpec)
|
||||
r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", m.deleteSpec)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Mount) notFound(w http.ResponseWriter, r *http.Request) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
}
|
||||
|
||||
func NewMount(agentRepo datastore.AgentRepository, authenticators ...auth.Authenticator) *Mount {
|
||||
return &Mount{agentRepo, authenticators}
|
||||
}
|
100
internal/server/api/query_agents.go
Normal file
100
internal/server/api/query_agents.go
Normal file
@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (m *Mount) queryAgents(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
offset, ok := getIntQueryParam(w, r, "offset", 0)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
options := []datastore.AgentQueryOptionFunc{
|
||||
datastore.WithAgentQueryLimit(int(limit)),
|
||||
datastore.WithAgentQueryOffset(int(offset)),
|
||||
datastore.WithAgentQueryTenantID(user.Tenant()),
|
||||
}
|
||||
|
||||
ids, ok := getIntSliceValues(w, r, "ids", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if ids != nil {
|
||||
agentIDs := func(ids []int64) []datastore.AgentID {
|
||||
agentIDs := make([]datastore.AgentID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
agentIDs = append(agentIDs, datastore.AgentID(id))
|
||||
}
|
||||
|
||||
return agentIDs
|
||||
}(ids)
|
||||
|
||||
options = append(options, datastore.WithAgentQueryID(agentIDs...))
|
||||
}
|
||||
|
||||
thumbprints, ok := getStringSliceValues(w, r, "thumbprints", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if thumbprints != nil {
|
||||
options = append(options, datastore.WithAgentQueryThumbprints(thumbprints...))
|
||||
}
|
||||
|
||||
statuses, ok := getIntSliceValues(w, r, "statuses", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if statuses != nil {
|
||||
agentStatuses := func(statuses []int64) []datastore.AgentStatus {
|
||||
agentStatuses := make([]datastore.AgentStatus, 0, len(statuses))
|
||||
for _, status := range statuses {
|
||||
agentStatuses = append(agentStatuses, datastore.AgentStatus(status))
|
||||
}
|
||||
|
||||
return agentStatuses
|
||||
}(statuses)
|
||||
|
||||
options = append(options, datastore.WithAgentQueryStatus(agentStatuses...))
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
agents, total, err := m.agentRepo.Query(
|
||||
ctx,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not list agents", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agents []*datastore.Agent `json:"agents"`
|
||||
Total int `json:"total"`
|
||||
}{
|
||||
Agents: agents,
|
||||
Total: total,
|
||||
})
|
||||
}
|
151
internal/server/api/register_agent.go
Normal file
151
internal/server/api/register_agent.go
Normal file
@ -0,0 +1,151 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type registerAgentRequest struct {
|
||||
KeySet json.RawMessage `json:"keySet" validate:"required"`
|
||||
Metadata []metadata.Tuple `json:"metadata" validate:"required"`
|
||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||
Signature string `json:"signature" validate:"required"`
|
||||
}
|
||||
|
||||
func (m *Mount) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
registerAgentReq := ®isterAgentRequest{}
|
||||
if ok := api.Bind(w, r, registerAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
keySet, err := jwk.Parse(registerAgentReq.KeySet)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not parse key set", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint))
|
||||
|
||||
// Validate that the existing signature validates the request
|
||||
|
||||
validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not validate signature", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !validSignature {
|
||||
logger.Warn(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature))
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
metadata := metadata.FromSorted(registerAgentReq.Metadata)
|
||||
|
||||
agent, err := m.agentRepo.Create(
|
||||
ctx,
|
||||
registerAgentReq.Thumbprint,
|
||||
keySet,
|
||||
metadata,
|
||||
)
|
||||
if err != nil {
|
||||
if !errors.Is(err, datastore.ErrAlreadyExist) {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not create agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agents, _, err := m.agentRepo.Query(
|
||||
ctx,
|
||||
datastore.WithAgentQueryThumbprints(registerAgentReq.Thumbprint),
|
||||
datastore.WithAgentQueryLimit(1),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not retrieve agents", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(agents) == 0 {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not retrieve matching agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agentID := agents[0].ID
|
||||
|
||||
agent, err = m.agentRepo.Get(ctx, agentID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(
|
||||
ctx, "could not retrieve agent",
|
||||
logger.CapturedE(err), logger.F("agentID", agentID),
|
||||
)
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not validate signature using previous keyset", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !validSignature {
|
||||
logger.Error(ctx, "invalid signature")
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent, err = m.agentRepo.Update(
|
||||
ctx,
|
||||
agents[0].ID,
|
||||
datastore.WithAgentUpdateKeySet(keySet),
|
||||
datastore.WithAgentUpdateMetadata(metadata),
|
||||
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusCreated, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
58
internal/server/api/release_agent.go
Normal file
58
internal/server/api/release_agent.go
Normal file
@ -0,0 +1,58 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type releaseAgentRequest struct {
|
||||
AgentID int64 `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
func (m *Mount) releaseAgent(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
releaseAgentReq := &releaseAgentRequest{}
|
||||
if ok := api.Bind(w, r, releaseAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
agentID := datastore.AgentID(releaseAgentReq.AgentID)
|
||||
|
||||
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
agent, err := m.agentRepo.Get(ctx, agentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not retrieve agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent, err = m.agentRepo.Detach(ctx, agent.ID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not detach agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
58
internal/server/api/update_agent.go
Normal file
58
internal/server/api/update_agent.go
Normal file
@ -0,0 +1,58 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type updateAgentRequest struct {
|
||||
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
||||
Label *string `json:"label" validate:"omitempty"`
|
||||
}
|
||||
|
||||
func (m *Mount) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
updateAgentReq := &updateAgentRequest{}
|
||||
if ok := api.Bind(w, r, updateAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
options := make([]datastore.AgentUpdateOptionFunc, 0)
|
||||
|
||||
if updateAgentReq.Status != nil {
|
||||
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
||||
}
|
||||
|
||||
if updateAgentReq.Label != nil {
|
||||
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
||||
}
|
||||
|
||||
agent, err := m.agentRepo.Update(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
86
internal/server/api/update_spec.go
Normal file
86
internal/server/api/update_spec.go
Normal file
@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
|
||||
)
|
||||
|
||||
type updateSpecRequest struct {
|
||||
spec.RawSpec
|
||||
}
|
||||
|
||||
func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
updateSpecReq := &updateSpecRequest{}
|
||||
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := spec.Validate(ctx, updateSpecReq); err != nil {
|
||||
data := struct {
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
|
||||
var validationErr *spec.ValidationError
|
||||
|
||||
if errors.As(err, &validationErr) {
|
||||
data.Message = validationErr.Error()
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not validate spec", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
spec, err := m.agentRepo.UpdateSpec(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
string(updateSpecReq.SpecName()),
|
||||
updateSpecReq.SpecRevision(),
|
||||
updateSpecReq.SpecData(),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, datastore.ErrUnexpectedRevision) {
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeUnexpectedRevision, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not update spec", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Spec *datastore.Spec `json:"spec"`
|
||||
}{
|
||||
Spec: spec,
|
||||
})
|
||||
}
|
@ -10,12 +10,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
||||
"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"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/server/api"
|
||||
"github.com/antonmedv/expr"
|
||||
"github.com/antonmedv/expr/vm"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@ -101,28 +101,13 @@ 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)
|
||||
apiMount := api.NewMount(
|
||||
s.agentRepo,
|
||||
thirdPartyAuth,
|
||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||
)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
thirdPartyAuth,
|
||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||
))
|
||||
|
||||
r.Route("/agents", func(r chi.Router) {
|
||||
r.Post("/claim", s.claimAgent)
|
||||
|
||||
r.With(assertGlobalReadAccess).Get("/", s.queryAgents)
|
||||
|
||||
r.With(assertAgentReadAccess).Get("/{agentID}", s.getAgent)
|
||||
r.With(assertAgentWriteAccess).Put("/{agentID}", s.updateAgent)
|
||||
r.With(assertAgentWriteAccess).Delete("/{agentID}", s.deleteAgent)
|
||||
|
||||
r.With(assertAgentReadAccess).Get("/{agentID}/specs", s.getAgentSpecs)
|
||||
r.With(assertAgentWriteAccess).Post("/{agentID}/specs", s.updateSpec)
|
||||
r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", s.deleteSpec)
|
||||
})
|
||||
})
|
||||
apiMount.Mount(r)
|
||||
})
|
||||
|
||||
logger.Info(ctx, "http server listening")
|
||||
|
@ -1,179 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
|
||||
)
|
||||
|
||||
type updateSpecRequest struct {
|
||||
spec.RawSpec
|
||||
}
|
||||
|
||||
func (s *Server) updateSpec(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
updateSpecReq := &updateSpecRequest{}
|
||||
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := spec.Validate(ctx, updateSpecReq); err != nil {
|
||||
data := struct {
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
|
||||
var validationErr *spec.ValidationError
|
||||
|
||||
if errors.As(err, &validationErr) {
|
||||
data.Message = validationErr.Error()
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not validate spec", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
spec, err := s.agentRepo.UpdateSpec(
|
||||
ctx,
|
||||
datastore.AgentID(agentID),
|
||||
string(updateSpecReq.SpecName()),
|
||||
updateSpecReq.SpecRevision(),
|
||||
updateSpecReq.SpecData(),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, datastore.ErrUnexpectedRevision) {
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeUnexpectedRevision, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not update spec", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Spec *datastore.Spec `json:"spec"`
|
||||
}{
|
||||
Spec: spec,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
specs, err := s.agentRepo.GetSpecs(ctx, agentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not list specs", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Specs []*datastore.Spec `json:"specs"`
|
||||
}{
|
||||
Specs: specs,
|
||||
})
|
||||
}
|
||||
|
||||
type deleteSpecRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (s *Server) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
deleteSpecReq := &deleteSpecRequest{}
|
||||
if ok := api.Bind(w, r, deleteSpecReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
err := s.agentRepo.DeleteSpec(
|
||||
ctx,
|
||||
agentID,
|
||||
deleteSpecReq.Name,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not delete spec", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
Name: deleteSpecReq.Name,
|
||||
})
|
||||
}
|
||||
|
||||
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
||||
rawSpecID := chi.URLParam(r, "")
|
||||
|
||||
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(r.Context(), "could not parse spec id", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return datastore.SpecID(specID), true
|
||||
}
|
Loading…
Reference in New Issue
Block a user