Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement AccessorID resolution and client ip propagation #19

Merged
merged 3 commits into from
Mar 4, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Enrich the data with Token Polices, AccessorID and Client IP
ncode committed Feb 3, 2025
commit 7ef1e46fef3d065b7f429e0354d628c5c85e11b7
50 changes: 29 additions & 21 deletions admissionctrl/controller.go
Original file line number Diff line number Diff line change
@@ -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"
@@ -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
}
@@ -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)
@@ -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)
@@ -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)
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"
@@ -10,7 +11,6 @@ import (
)

func TestJobHandler_ApplyAdmissionControllers(t *testing.T) {

type fields struct {
mutator JobMutator
validator JobValidator
@@ -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",
@@ -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 {
@@ -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
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/mxab/nacp/admissionctrl/types"
"net/http"
"net/url"

@@ -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
@@ -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))
}
@@ -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 {
4 changes: 3 additions & 1 deletion admissionctrl/mutator/json_patch_webhook_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package mutator

import (
"fmt"
"github.com/mxab/nacp/admissionctrl/types"
"net/http"
"net/http/httptest"
"testing"
@@ -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)
18 changes: 9 additions & 9 deletions admissionctrl/mutator/opa_json_patch.go
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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()))
@@ -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()))
}
@@ -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
}
@@ -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
4 changes: 3 additions & 1 deletion admissionctrl/mutator/opa_json_patch_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package mutator

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

"github.com/hashicorp/go-hclog"
@@ -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)
26 changes: 20 additions & 6 deletions admissionctrl/mutator/webhook_mutator.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ package mutator
import (
"bytes"
"encoding/json"
"github.com/mxab/nacp/admissionctrl/types"
"io"
"net/http"
"net/url"

@@ -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{}
Loading