Skip to content

Commit

Permalink
Enrich the data with Token Polices, AccessorID and Client IP
Browse files Browse the repository at this point in the history
  • Loading branch information
ncode committed Feb 3, 2025
1 parent ff7c913 commit 8ca18ea
Show file tree
Hide file tree
Showing 25 changed files with 414 additions and 174 deletions.
50 changes: 29 additions & 21 deletions admissionctrl/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package admissionctrl
import (
"encoding/json"
"fmt"
"github.com/mxab/nacp/admissionctrl/types"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
Expand All @@ -18,37 +19,39 @@ type AdmissionController interface {

type JobMutator interface {
AdmissionController
Mutate(*api.Job) (out *api.Job, warnings []error, err error)
Mutate(*types.Payload) (*api.Job, []error, error)
}

type JobValidator interface {
AdmissionController
Validate(*api.Job) (warnings []error, err error)
Validate(*types.Payload) (warnings []error, err error)
}

type JobHandler struct {
mutators []JobMutator
validators []JobValidator
logger hclog.Logger
mutators []JobMutator
validators []JobValidator
resolveToken bool
logger hclog.Logger
}

func NewJobHandler(mutators []JobMutator, validators []JobValidator, logger hclog.Logger) *JobHandler {
func NewJobHandler(mutators []JobMutator, validators []JobValidator, logger hclog.Logger, resolverToken bool) *JobHandler {
return &JobHandler{
mutators: mutators,
validators: validators,
logger: logger,
mutators: mutators,
validators: validators,
logger: logger,
resolveToken: resolverToken,
}
}

func (j *JobHandler) ApplyAdmissionControllers(job *api.Job) (out *api.Job, warnings []error, err error) {
func (j *JobHandler) ApplyAdmissionControllers(payload *types.Payload) (out *api.Job, warnings []error, err error) {
// Mutators run first before validators, so validators view the final rendered job.
// So, mutators must handle invalid jobs.
out, warnings, err = j.AdmissionMutators(job)
out, warnings, err = j.AdmissionMutators(payload)
if err != nil {
return nil, nil, err
}

validateWarnings, err := j.AdmissionValidators(job)
validateWarnings, err := j.AdmissionValidators(payload)
if err != nil {
return nil, nil, err
}
Expand All @@ -57,13 +60,14 @@ func (j *JobHandler) ApplyAdmissionControllers(job *api.Job) (out *api.Job, warn
return out, warnings, nil
}

// admissionMutator returns an updated job as well as warnings or an error.
func (j *JobHandler) AdmissionMutators(job *api.Job) (_ *api.Job, warnings []error, err error) {
// AdmissionMutators returns an updated job as well as warnings or an error.
func (j *JobHandler) AdmissionMutators(payload *types.Payload) (job *api.Job, warnings []error, err error) {
var w []error
j.logger.Debug("applying job mutators", "mutators", len(j.mutators), "job", job.ID)
job = payload.Job
j.logger.Debug("applying job mutators", "mutators", len(j.mutators), "job", payload.Job.ID)
for _, mutator := range j.mutators {
j.logger.Debug("applying job mutator", "mutator", mutator.Name(), "job", job.ID)
job, w, err = mutator.Mutate(job)
j.logger.Debug("applying job mutator", "mutator", mutator.Name(), "job", payload.Job.ID)
job, w, err = mutator.Mutate(payload)
j.logger.Trace("job mutate results", "mutator", mutator.Name(), "warnings", w, "error", err)
if err != nil {
return nil, nil, fmt.Errorf("error in job mutator %s: %v", mutator.Name(), err)
Expand All @@ -75,17 +79,17 @@ func (j *JobHandler) AdmissionMutators(job *api.Job) (_ *api.Job, warnings []err

// AdmissionValidators returns a slice of validation warnings and a multierror
// of validation failures.
func (j *JobHandler) AdmissionValidators(origJob *api.Job) ([]error, error) {
func (j *JobHandler) AdmissionValidators(payload *types.Payload) ([]error, error) {
// ensure job is not mutated
j.logger.Debug("applying job validators", "validators", len(j.validators), "job", origJob.ID)
job := copyJob(origJob)
j.logger.Debug("applying job validators", "validators", len(j.validators), "job", payload.Job.ID)
job := copyJob(payload.Job)

var warnings []error
var errs error

for _, validator := range j.validators {
j.logger.Debug("applying job validator", "validator", validator.Name(), "job", job.ID)
w, err := validator.Validate(job)
w, err := validator.Validate(payload)
j.logger.Trace("job validate results", "validator", validator.Name(), "warnings", w, "error", err)
if err != nil {
errs = multierror.Append(errs, err)
Expand All @@ -97,6 +101,10 @@ func (j *JobHandler) AdmissionValidators(origJob *api.Job) ([]error, error) {

}

func (j *JobHandler) ResolveToken() bool {
return j.resolveToken
}

func copyJob(job *api.Job) *api.Job {
jobCopy := &api.Job{}
data, err := json.Marshal(job)
Expand Down
27 changes: 15 additions & 12 deletions admissionctrl/controller_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package admissionctrl

import (
"github.com/mxab/nacp/admissionctrl/types"
"testing"

"github.com/hashicorp/go-hclog"
Expand All @@ -10,7 +11,6 @@ import (
)

func TestJobHandler_ApplyAdmissionControllers(t *testing.T) {

type fields struct {
mutator JobMutator
validator JobValidator
Expand All @@ -19,18 +19,21 @@ func TestJobHandler_ApplyAdmissionControllers(t *testing.T) {
job *api.Job
}
job := &api.Job{} // testutil.ReadJob(t)
payload := &types.Payload{Job: job}
mutator := new(testutil.MockMutator)
mutator.On("Mutate", job).Return(job, []error{}, nil)
mutator.On("Mutate", payload).Return(payload.Job, []error{}, nil)

validator := new(testutil.MockValidator)
validator.On("Validate", job).Return([]error{}, nil)
validator.On("Validate", payload).Return([]error{}, nil)

tests := []struct {
name string
fields fields
args args
want *api.Job
want1 []error
wantErr bool
name string
fields fields
args args
want *api.Job
want1 []error
wantErr bool
resolveToken bool
}{
{
name: "test",
Expand All @@ -49,8 +52,9 @@ func TestJobHandler_ApplyAdmissionControllers(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
j := NewJobHandler([]JobMutator{tt.fields.mutator}, []JobValidator{tt.fields.validator}, hclog.NewNullLogger())
_, warnings, err := j.ApplyAdmissionControllers(tt.args.job)
j := NewJobHandler([]JobMutator{tt.fields.mutator}, []JobValidator{tt.fields.validator}, hclog.NewNullLogger(), tt.resolveToken)
payload := &types.Payload{Job: tt.args.job}
_, warnings, err := j.ApplyAdmissionControllers(payload)
assert.Empty(t, warnings, "No Warnings")

if (err != nil) != tt.wantErr {
Expand All @@ -66,7 +70,6 @@ func TestJobHandler_ApplyAdmissionControllers(t *testing.T) {

mutator.AssertExpectations(t)
validator.AssertExpectations(t)

})
}
}
25 changes: 19 additions & 6 deletions admissionctrl/mutator/json_patch_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/mxab/nacp/admissionctrl/types"
"net/http"
"net/url"

Expand Down Expand Up @@ -36,18 +37,30 @@ func NewJsonPatchWebhookMutator(name string, endpoint string, method string, log
method: method,
}, nil
}
func (j *JsonPatchWebhookMutator) Mutate(job *api.Job) (*api.Job, []error, error) {

jobJson, err := json.Marshal(job)
func (j *JsonPatchWebhookMutator) Mutate(payload *types.Payload) (*api.Job, []error, error) {
jobJson, err := json.Marshal(payload)
if err != nil {
return nil, nil, err
}
httpClient := &http.Client{}

req, err := http.NewRequest(j.method, j.endpoint.String(), bytes.NewBuffer(jobJson))
if err != nil {
return nil, nil, err
}

// Add context headers and body if available
if payload.Context != nil {
// Add standard headers for backward compatibility
if payload.Context.ClientIP != "" {
req.Header.Set("X-Forwarded-For", payload.Context.ClientIP) // Standard proxy header
req.Header.Set("NACP-Client-IP", payload.Context.ClientIP) // NACP specific
}
if payload.Context.AccessorID != "" {
req.Header.Set("NACP-Accessor-ID", payload.Context.AccessorID)
}
}

httpClient := &http.Client{}
res, err := httpClient.Do(req)
if err != nil {
return nil, nil, err
Expand All @@ -61,7 +74,7 @@ func (j *JsonPatchWebhookMutator) Mutate(job *api.Job) (*api.Job, []error, error

var warnings []error
if len(patchResponse.Warnings) > 0 {
j.logger.Debug("Got errors from rule", "rule", j.name, "warnings", patchResponse.Warnings, "job", job.ID)
j.logger.Debug("Got errors from rule", "rule", j.name, "warnings", patchResponse.Warnings, "job", payload.Job.ID)
for _, warning := range patchResponse.Warnings {
warnings = append(warnings, fmt.Errorf(warning))
}
Expand All @@ -75,7 +88,7 @@ func (j *JsonPatchWebhookMutator) Mutate(job *api.Job) (*api.Job, []error, error
if err != nil {
return nil, nil, err
}
j.logger.Debug("Got patch fom rule", "rule", j.name, "patch", string(patchJson), "job", job.ID)
j.logger.Debug("Got patch fom rule", "rule", j.name, "patch", string(patchJson), "job", payload.Job.ID)
patchedJobJson, err := patch.Apply(jobJson)

if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion admissionctrl/mutator/json_patch_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mutator

import (
"fmt"
"github.com/mxab/nacp/admissionctrl/types"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -95,7 +96,8 @@ func TestJsonPatchMutator(t *testing.T) {
mutator, err := NewJsonPatchWebhookMutator(tc.name, webhookServer.URL+tc.endpointPath, tc.method, hclog.NewNullLogger())
require.NoError(t, err)

job, warnings, err := mutator.Mutate(tc.job)
payload := &types.Payload{Job: tc.job}
job, warnings, err := mutator.Mutate(payload)

require.True(t, webhookCalled)
assert.Equal(t, tc.wantErr, err)
Expand Down
18 changes: 9 additions & 9 deletions admissionctrl/mutator/opa_json_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import (
"context"
"encoding/json"
"fmt"

jsonpatch "github.com/evanphx/json-patch"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/api"
"github.com/mxab/nacp/admissionctrl/notation"
"github.com/mxab/nacp/admissionctrl/opa"
"github.com/mxab/nacp/admissionctrl/types"
)

type OpaJsonPatchMutator struct {
Expand All @@ -19,19 +19,19 @@ type OpaJsonPatchMutator struct {
name string
}

func (j *OpaJsonPatchMutator) Mutate(job *api.Job) (*api.Job, []error, error) {
func (j *OpaJsonPatchMutator) Mutate(payload *types.Payload) (*api.Job, []error, error) {
allWarnings := make([]error, 0)
ctx := context.TODO()

results, err := j.query.Query(ctx, job)
results, err := j.query.Query(ctx, payload)
if err != nil {
return nil, nil, err
}

errors := results.GetErrors()

if len(errors) > 0 {
j.logger.Debug("Got errors from rule", "rule", j.Name(), "errors", errors, "job", job.ID)
j.logger.Debug("Got errors from rule", "rule", j.Name(), "errors", errors, "job", payload.Job.ID)
allErrors := multierror.Append(nil)
for _, warn := range errors {
allErrors = multierror.Append(allErrors, fmt.Errorf("%s (%s)", warn, j.Name()))
Expand All @@ -42,7 +42,7 @@ func (j *OpaJsonPatchMutator) Mutate(job *api.Job) (*api.Job, []error, error) {
warnings := results.GetWarnings()

if len(warnings) > 0 {
j.logger.Debug("Got warnings from rule", "rule", j.Name(), "warnings", warnings, "job", job.ID)
j.logger.Debug("Got warnings from rule", "rule", j.Name(), "warnings", warnings, "job", payload.Job.ID)
for _, warn := range warnings {
allWarnings = append(allWarnings, fmt.Errorf("%s (%s)", warn, j.Name()))
}
Expand All @@ -57,8 +57,8 @@ func (j *OpaJsonPatchMutator) Mutate(job *api.Job) (*api.Job, []error, error) {
if err != nil {
return nil, nil, err
}
j.logger.Debug("Got patch fom rule", "rule", j.Name(), "patch", string(patchJSON), "job", job.ID)
jobJson, err := json.Marshal(job)
j.logger.Debug("Got patch fom rule", "rule", j.Name(), "patch", string(patchJSON), "job", payload.Job.ID)
jobJson, err := json.Marshal(payload.Job)
if err != nil {
return nil, nil, err
}
Expand All @@ -72,9 +72,9 @@ func (j *OpaJsonPatchMutator) Mutate(job *api.Job) (*api.Job, []error, error) {
if err != nil {
return nil, nil, err
}
job = &patchedJob
payload.Job = &patchedJob

return job, allWarnings, nil
return payload.Job, allWarnings, nil
}
func (j *OpaJsonPatchMutator) Name() string {
return j.name
Expand Down
4 changes: 3 additions & 1 deletion admissionctrl/mutator/opa_json_patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mutator

import (
"fmt"
"github.com/mxab/nacp/admissionctrl/types"
"testing"

"github.com/hashicorp/go-hclog"
Expand Down Expand Up @@ -90,7 +91,8 @@ func TestJSONPatcher_Mutate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotOut, gotWarnings, err := tt.j.Mutate(tt.args.job)
payload := &types.Payload{Job: tt.args.job}
gotOut, gotWarnings, err := tt.j.Mutate(payload)
require.Equal(t, tt.wantErr, err != nil, "JSONPatcher.Mutate() error = %v, wantErr %v", err, tt.wantErr)

assert.Equal(t, tt.wantWarnings, gotWarnings, "JSONPatcher.Mutate() gotWarnings = %v, want %v", gotWarnings, tt.wantWarnings)
Expand Down
26 changes: 20 additions & 6 deletions admissionctrl/mutator/webhook_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package mutator
import (
"bytes"
"encoding/json"
"github.com/mxab/nacp/admissionctrl/types"
"io"
"net/http"
"net/url"

Expand All @@ -15,18 +17,30 @@ type WebhookMutator struct {
method string
}

func (w *WebhookMutator) Mutate(job *api.Job) (out *api.Job, warnings []error, err error) {

data, err := json.Marshal(job)
func (w *WebhookMutator) Mutate(payload *types.Payload) (out *api.Job, warnings []error, err error) {
data, err := json.Marshal(payload)
if err != nil {
return nil, nil, err
}
buffer := bytes.NewBuffer(data)

req, err := http.NewRequest(w.method, w.endpoint.String(), buffer)
req, err := http.NewRequest(w.method, w.endpoint.String(), bytes.NewBuffer(data))
if err != nil {
return nil, nil, err
}

// Add context headers and body if available
if payload.Context != nil {
// Add standard headers for backward compatibility
if payload.Context.ClientIP != "" {
req.Header.Set("X-Forwarded-For", payload.Context.ClientIP) // Standard proxy header
req.Header.Set("NACP-Client-IP", payload.Context.ClientIP) // NACP specific
}
if payload.Context.AccessorID != "" {
req.Header.Set("NACP-Accessor-ID", payload.Context.AccessorID)
}
}

req.Body = io.NopCloser(bytes.NewBuffer(data))
req.ContentLength = int64(len(data))
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
Expand Down
Loading

0 comments on commit 8ca18ea

Please sign in to comment.