feat: resources segregation by tenant
All checks were successful
arcad/emissary/pipeline/head This commit looks good
arcad/emissary/pipeline/pr-master This commit looks good

This commit is contained in:
2024-02-26 18:20:40 +01:00
parent 79f53010a0
commit ca4211daef
45 changed files with 704 additions and 429 deletions

View File

@ -20,6 +20,8 @@ const (
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 {
@ -130,7 +132,8 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
}
agent, err = s.agentRepo.Update(
ctx, agents[0].ID,
ctx,
agents[0].ID,
datastore.WithAgentUpdateKeySet(keySet),
datastore.WithAgentUpdateMetadata(metadata),
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
@ -201,6 +204,11 @@ func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
}
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
@ -214,6 +222,7 @@ func (s *Server) queryAgents(w http.ResponseWriter, r *http.Request) {
options := []datastore.AgentQueryOptionFunc{
datastore.WithAgentQueryLimit(int(limit)),
datastore.WithAgentQueryOffset(int(offset)),
datastore.WithAgentQueryTenantID(user.Tenant()),
}
ids, ok := getIntSliceValues(w, r, "ids", nil)
@ -290,6 +299,10 @@ func (s *Server) deleteAgent(w http.ResponseWriter, r *http.Request) {
return
}
if ok := s.assertTenantOwns(w, r, agentID); !ok {
return
}
ctx := r.Context()
err := s.agentRepo.Delete(
@ -323,6 +336,10 @@ func (s *Server) getAgent(w http.ResponseWriter, r *http.Request) {
return
}
if ok := s.assertTenantOwns(w, r, agentID); !ok {
return
}
ctx := r.Context()
agent, err := s.agentRepo.Get(
@ -350,6 +367,73 @@ func (s *Server) getAgent(w http.ResponseWriter, r *http.Request) {
})
}
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")

View File

@ -8,6 +8,7 @@ import (
"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/datastore"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
@ -132,7 +133,7 @@ func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool)
return nil, false
}
if user == nil {
if user == nil || user.Tenant() == "" {
forbidden(w, r)
return nil, false
@ -141,6 +142,30 @@ 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 {
ctx := r.Context()
user, ok := assertRequestUser(w, r)
if !ok {
return false
}
agent, err := s.agentRepo.Get(ctx, agentID)
if err != nil {
err = errors.WithStack(err)
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
}
if agent.TenantID != nil && *agent.TenantID == user.Tenant() {
return true
}
api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil)
return false
}
func forbidden(w http.ResponseWriter, r *http.Request) {
logger.Warn(r.Context(), "forbidden", logger.F("path", r.URL.Path))

View File

@ -110,7 +110,10 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
))
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)
@ -205,18 +208,16 @@ func (s *Server) getThirdPartyAuthenticator() (*thirdparty.Authenticator, error)
return nil, errors.WithStack(err)
}
return thirdparty.NewAuthenticator(getKeySet, getTokenRole, thirdparty.DefaultAcceptableSkew), nil
}
func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
rawRules := s.conf.Auth.RoleExtractionRules
rules := make([]*vm.Program, 0, len(rawRules))
type Env struct {
JWT map[string]any `expr:"jwt"`
getTenantRole, err := s.createGetTokenTenantFunc()
if err != nil {
return nil, errors.WithStack(err)
}
strFunc := expr.Function(
return thirdparty.NewAuthenticator(getKeySet, getTokenRole, getTenantRole, thirdparty.DefaultAcceptableSkew), nil
}
var ruleFuncs = []expr.Option{
expr.Function(
"str",
func(params ...any) (any, error) {
var builder strings.Builder
@ -230,14 +231,24 @@ func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.T
return builder.String(), nil
},
new(func(any) string),
)
),
}
func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
rawRules := s.conf.Auth.RoleExtractionRules
rules := make([]*vm.Program, 0, len(rawRules))
type Env struct {
JWT map[string]any `expr:"jwt"`
}
opts := append([]expr.Option{
expr.Env(Env{}),
expr.AsKind(reflect.String),
}, ruleFuncs...)
for _, rr := range rawRules {
r, err := expr.Compile(rr,
expr.Env(Env{}),
expr.AsKind(reflect.String),
strFunc,
)
r, err := expr.Compile(rr, opts...)
if err != nil {
return nil, errors.Wrapf(err, "could not compile role extraction rule '%s'", rr)
}
@ -276,6 +287,59 @@ func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.T
}, nil
}
func (s *Server) createGetTokenTenantFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
rawRules := s.conf.Auth.TenantExtractionRules
rules := make([]*vm.Program, 0, len(rawRules))
type Env struct {
JWT map[string]any `expr:"jwt"`
}
opts := append([]expr.Option{
expr.Env(Env{}),
expr.AsKind(reflect.String),
}, ruleFuncs...)
for _, rr := range rawRules {
r, err := expr.Compile(rr, opts...)
if err != nil {
return nil, errors.Wrapf(err, "could not compile role extraction rule '%s'", rr)
}
rules = append(rules, r)
}
return func(ctx context.Context, token jwt.Token) (string, error) {
jwt, err := token.AsMap(ctx)
if err != nil {
return "", errors.WithStack(err)
}
vm := vm.VM{}
for _, r := range rules {
result, err := vm.Run(r, Env{
JWT: jwt,
})
if err != nil {
return "", errors.WithStack(err)
}
tenant, ok := result.(string)
if !ok {
logger.Debug(ctx, "ignoring unexpected tenant extraction result", logger.F("result", result))
continue
}
if tenant != "" {
return tenant, nil
}
}
return "", errors.New("could not extract tenant from token")
}, nil
}
func New(funcs ...OptionFunc) *Server {
opt := defaultOption()
for _, fn := range funcs {