feat: resources segregation by tenant
This commit is contained in:
@ -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")
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user