Compare commits

...

5 Commits

Author SHA1 Message Date
0b34b485da feat(server): assert agent is accepted for api operations
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2024-03-04 19:03:17 +01:00
ab08d30d2a feat(server): allow registering renewal for forgotten agents
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2024-03-04 18:52:19 +01:00
f6ffb68c43 feat(client): show response body on json parsing error
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2024-03-04 18:51:36 +01:00
4a1a434556 fix(migrations): disable foreign keys for migrating tenants
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2024-03-04 09:09:44 +01:00
76718722cc feat(server): add /api/v1/session endpoint
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2024-03-03 18:40:56 +01:00
9 changed files with 113 additions and 27 deletions

View File

@ -1,6 +1,7 @@
package agent package agent
import ( import (
"encoding/json"
"fmt" "fmt"
"forge.cadoles.com/Cadoles/emissary/internal/auth" "forge.cadoles.com/Cadoles/emissary/internal/auth"
@ -29,4 +30,18 @@ func (u *User) Agent() *datastore.Agent {
return u.agent return u.agent
} }
func (u *User) MarshalJSON() ([]byte, error) {
type user struct {
Subject string `json:"subject"`
Tenant string `json:"tenant"`
}
jsonUser := user{
Subject: u.Subject(),
Tenant: string(u.Tenant()),
}
return json.Marshal(jsonUser)
}
var _ auth.User = &User{} var _ auth.User = &User{}

View File

@ -64,16 +64,16 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
} }
if user == nil { if user == nil {
isUnauthorized, isUnauthenticated, isUnknown := checkErrors(errs) hasUnauthorized, hasUnauthenticated, hasUnknown := checkErrors(errs)
switch { switch {
case isUnauthorized && !isUnknown: case hasUnauthorized && !hasUnknown:
api.ErrorResponse(w, http.StatusForbidden, api.ErrCodeForbidden, nil) api.ErrorResponse(w, http.StatusForbidden, api.ErrCodeForbidden, nil)
return return
case isUnauthenticated && !isUnknown: case hasUnauthenticated && !hasUnknown:
api.ErrorResponse(w, http.StatusForbidden, api.ErrCodeForbidden, nil) api.ErrorResponse(w, http.StatusUnauthorized, api.ErrCodeUnauthorized, nil)
return return
case isUnknown: case hasUnknown:
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil) api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
return return
default: default:
@ -101,10 +101,8 @@ func checkErrors(errs []error) (isUnauthorized bool, isUnauthenticated bool, isU
switch { switch {
case errors.Is(e, ErrUnauthorized): case errors.Is(e, ErrUnauthorized):
isUnauthorized = true isUnauthorized = true
continue
case errors.Is(e, ErrUnauthenticated): case errors.Is(e, ErrUnauthenticated):
isUnauthenticated = true isUnauthenticated = true
continue
default: default:
isUnknown = true isUnknown = true
} }

View File

@ -1,6 +1,8 @@
package user package user
import ( import (
"encoding/json"
"forge.cadoles.com/Cadoles/emissary/internal/auth" "forge.cadoles.com/Cadoles/emissary/internal/auth"
"forge.cadoles.com/Cadoles/emissary/internal/datastore" "forge.cadoles.com/Cadoles/emissary/internal/datastore"
) )
@ -39,4 +41,20 @@ func (u *User) Role() Role {
return u.role return u.role
} }
func (u *User) MarshalJSON() ([]byte, error) {
type user struct {
Subject string `json:"subject"`
Tenant string `json:"tenant"`
Role string `json:"role"`
}
jsonUser := user{
Subject: u.Subject(),
Tenant: string(u.Tenant()),
Role: string(u.Role()),
}
return json.Marshal(jsonUser)
}
var _ auth.User = &User{} var _ auth.User = &User{}

View File

@ -183,7 +183,7 @@ func assertMatchingAgent() assertAgent {
} }
agent := u.Agent() agent := u.Agent()
if agent != nil && agent.ID == agentID { if agent != nil && agent.ID == agentID && agent.Status == datastore.AgentStatusAccepted {
return true return true
} }

View File

@ -0,0 +1,35 @@
package api
import (
"net/http"
"forge.cadoles.com/Cadoles/emissary/internal/auth"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
func (m *Mount) getSession(w http.ResponseWriter, r *http.Request) {
user, ok := assertRequestUser(w, r)
if !ok {
return
}
ctx := r.Context()
tenant, err := m.tenantRepo.Get(ctx, user.Tenant())
if err != nil && !errors.Is(err, datastore.ErrNotFound) {
err = errors.WithStack(err)
logger.Error(ctx, "could not retrieve user tenant", logger.CapturedE(err))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
}
api.DataResponse(w, http.StatusOK, struct {
User auth.User `json:"user"`
Tenant *datastore.Tenant `json:"tenant"`
}{
User: user,
Tenant: tenant,
})
}

View File

@ -23,6 +23,8 @@ func (m *Mount) Mount(r chi.Router) {
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(auth.Middleware(m.authenticators...)) r.Use(auth.Middleware(m.authenticators...))
r.Get("/session", m.getSession)
r.Route("/agents", func(r chi.Router) { r.Route("/agents", func(r chi.Router) {
r.With(assertUserWithWriteAccess).Post("/claim", m.claimAgent) r.With(assertUserWithWriteAccess).Post("/claim", m.claimAgent)

View File

@ -50,8 +50,8 @@ func (m *Mount) registerAgent(w http.ResponseWriter, r *http.Request) {
} }
if !validSignature { if !validSignature {
logger.Warn(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature)) logger.Warn(ctx, "invalid thumbprint signature", logger.F("signature", registerAgentReq.Signature))
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil) api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
return return
} }
@ -109,6 +109,7 @@ func (m *Mount) registerAgent(w http.ResponseWriter, r *http.Request) {
return return
} }
if agent.Status != datastore.AgentStatusForgotten {
validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata) validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
if err != nil { if err != nil {
err = errors.WithStack(err) err = errors.WithStack(err)
@ -121,17 +122,26 @@ func (m *Mount) registerAgent(w http.ResponseWriter, r *http.Request) {
if !validSignature { if !validSignature {
logger.Error(ctx, "invalid signature") logger.Error(ctx, "invalid signature")
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil) api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
return return
} }
}
updates := []datastore.AgentUpdateOptionFunc{
datastore.WithAgentUpdateKeySet(keySet),
datastore.WithAgentUpdateMetadata(metadata),
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
}
if agent.Status == datastore.AgentStatusForgotten {
updates = append(updates, datastore.WithAgentUpdateStatus(datastore.AgentStatusPending))
}
agent, err = m.agentRepo.Update( agent, err = m.agentRepo.Update(
ctx, ctx,
agents[0].ID, agents[0].ID,
datastore.WithAgentUpdateKeySet(keySet), updates...,
datastore.WithAgentUpdateMetadata(metadata),
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
) )
if err != nil { if err != nil {
err = errors.WithStack(err) err = errors.WithStack(err)

View File

@ -1,3 +1,5 @@
PRAGMA foreign_keys = 0;
CREATE TABLE tenants ( CREATE TABLE tenants (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
label TEXT NOT NULL, label TEXT NOT NULL,
@ -50,3 +52,5 @@ CREATE TABLE specs
INSERT INTO specs SELECT id, agent_id, name, revision, data, created_at, updated_at, 0 FROM _specs; INSERT INTO specs SELECT id, agent_id, name, revision, data, created_at, updated_at, 0 FROM _specs;
DROP TABLE _specs; DROP TABLE _specs;
PRAGMA foreign_keys = 1;

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -95,12 +96,15 @@ func (c *Client) apiDo(ctx context.Context, method string, path string, payload
defer res.Body.Close() defer res.Body.Close()
decoder := json.NewDecoder(res.Body) data, err := io.ReadAll(res.Body)
if err != nil {
if err := decoder.Decode(&response); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
if err := json.Unmarshal(data, &response); err != nil {
return errors.Wrapf(err, "could not parse json: got '%s'", data)
}
return nil return nil
} }