Writing raw policies

Raw policies are policies that can evaluate arbitrary JSON documents. For more information about raw policies, please refer to the raw policies page.

Examples

The following examples should look familiar if you completed the validation section of this tutorial.

Remember to mark the policy as raw by using the policyType field in the metadata.yml configuration. Please refer to the metadata specification for more information.

metadata.yml with policyType: raw
rules:
- apiGroups: [""]
  apiVersions: ["v1"]
  resources: ["pods"]
  operations: ["CREATE"]
mutating: false
contextAware: false
executionMode: kubewarden-wapc
policyType: raw
# Consider the policy for the background audit scans. Default is true. Note the
# intrinsic limitations of the background audit feature on docs.kubewarden.io;
# If your policy hits any limitations, set to false for the audit feature to
# skip this policy and not generate false positives.
backgroundAudit: true
annotations:
  # artifacthub specific:
  io.artifacthub.displayName: Policy Name
  io.artifacthub.resources: Pod
  io.artifacthub.keywords: pod, cool policy, kubewarden
  io.kubewarden.policy.ociUrl: ghcr.io/yourorg/policies/policy-name # must match release workflow oci-target
  # kubewarden specific:
  io.kubewarden.policy.title: policy-name
  io.kubewarden.policy.description: Short description
  io.kubewarden.policy.author: "Author name <author-email@example.com>"
  io.kubewarden.policy.url: https://github.com/yourorg/policy-name
  io.kubewarden.policy.source: https://github.com/yourorg/policy-name
  io.kubewarden.policy.license: Apache-2.0
  # The next two annotations are used in the policy report generated by the
  # Audit scanner. Severity indicates policy check result criticality and
  # Category indicates policy category. See more here at docs.kubewarden.io
  io.kubewarden.policy.severity: medium # one of info, low, medium, high, critical. See docs.
  io.kubewarden.policy.category: Resource validation

Validation

You want to write a policy that accepts a request in the following format:

{
  "request": {
    "user": "alice",
    "action": "delete",
    "resource": "products"
  }
}

and validates that:

  • user is in the list of valid users

  • action is in the list of valid actions

  • resource is in the list of valid resources

Start by scaffolding the policy by using the go policy template. Make sure everything is in place with a make, make test and make e2e-tests.

Firstly, define the types representing the payload of the request.

You need to declare a custom RawValidationRequest type (create a file request.go), containing the RawValidationRequest and the Settings structures, instead of using the ValidationRequest type provided by the SDK:

// RawValidationRequest represents the request that is sent to the validate function by the Policy Server.
type RawValidationRequest struct {
    Request Request `json:"request"`
    // Raw policies can have settings.
    Settings Settings `json:"settings"`
}

// Request represents the payload of the request.
type Request struct {
    User     string `json:"user"`
    Action   string `json:"action"`
    Resource string `json:"resource"`
}

Then you need to define the Settings type and the Valid and validateSettings functions in settings.go:

The Settings structure and the Valid and validateSettings functions in settings.go.
// Settings represents the settings of the policy.
type Settings struct {
	ValidUsers     []string `json:"validUsers"`
	ValidActions   []string `json:"validActions"`
	ValidResources []string `json:"validResources"`
}

// Valid returns true if the settings are valid.
func (s *Settings) Valid() (bool, error) {
    if len(s.ValidUsers) == 0 {
        return false, fmt.Errorf("validUsers cannot be empty")
    }

    if len(s.ValidActions) == 0 {
        return false, fmt.Errorf("validActions cannot be empty")
    }

    if len(s.ValidResources) == 0 {
        return false, fmt.Errorf("validResources cannot be empty")
    }

    return true, nil
}

// validateSettings validates the settings.
func validateSettings(payload []byte) ([]byte, error) {
    logger.Info("validating settings")

    settings := Settings{}
    err := json.Unmarshal(payload, &settings)
    if err != nil {
        return kubewarden.RejectSettings(kubewarden.Message(fmt.Sprintf("Provided settings are not valid: %v", err)))
    }

    valid, err := settings.Valid()
    if err != nil {
        return kubewarden.RejectSettings(kubewarden.Message(fmt.Sprintf("Provided settings are not valid: %v", err)))
    }
    if valid {
        return kubewarden.AcceptSettings()
    }

    logger.Warn("rejecting settings")
    return kubewarden.RejectSettings(kubewarden.Message("Provided settings are not valid"))
}

Finally, you replace the validate function (in validate.go):

The validate function in validate.go.
func validate(payload []byte) ([]byte, error) {
    // Unmarshal the payload into a RawValidationRequest instance
    validationRequest := RawValidationRequest{}
    err := json.Unmarshal(payload, &validationRequest)
    if err != nil {
        // If the payload is not valid, reject the request
        return kubewarden.RejectRequest(
            kubewarden.Message(err.Error()),
            kubewarden.Code(400))
    }

    request := validationRequest.Request
    settings := validationRequest.Settings

    // Validate the payload
    if slices.Contains(settings.ValidUsers, request.User) &&
        slices.Contains(settings.ValidActions, request.Action) &&
        slices.Contains(settings.ValidResources, request.Resource) {
        return kubewarden.AcceptRequest()
    }

    return kubewarden.RejectRequest(
        kubewarden.Message("The request cannot be accepted."),
        kubewarden.Code(400))
}

You can set up a test in e2e.bats:

e2e.bats
#!/usr/bin/env bats

@test "accept" {
  run kwctl run annotated-policy.wasm -r test_data/request.json -s test_data/settings.json

  # this prints the output when one the checks below fails
  echo "output = ${output}"

  # request allowed
  [ "$status" -eq 0 ]
  [ $(expr "$output" : '.*allowed.*true') -ne 0 ]
}

Then the outputs of make, make test and make e2e are:

Outputs
make && make test && make e2e-tests
docker run \
	--rm \
	-e GOFLAGS="-buildvcs=false" \
	-v /home/jhk/projects/suse/tmp/fab-goraw:/src \
	-w /src tinygo/tinygo:0.30.0 \
	tinygo build -o policy.wasm -target=wasi -no-debug .
go test -v
=== RUN   TestAcceptValidSettings
--- PASS: TestAcceptValidSettings (0.00s)
=== RUN   TestRejectSettingsWithEmptyValidUsers
--- PASS: TestRejectSettingsWithEmptyValidUsers (0.00s)
=== RUN   TestRejectSettingsWithEmptyValidActions
--- PASS: TestRejectSettingsWithEmptyValidActions (0.00s)
=== RUN   TestRejectSettingsWithEmptyValidResources
--- PASS: TestRejectSettingsWithEmptyValidResources (0.00s)
=== RUN   TestValidateRequestAccept
--- PASS: TestValidateRequestAccept (0.00s)
=== RUN   TestValidateRequestReject
--- PASS: TestValidateRequestReject (0.00s)
PASS
ok  	github.com/kubewarden/go-policy-template	0.002s
kwctl annotate -m metadata.yml -u README.md -o annotated-policy.wasm policy.wasm
bats e2e.bats
e2e.bats
 ✓ accept

1 test, 0 failures

Mutation

You need to change the earlier example to mutate the request instead of rejecting it. The settings should contain the defaultUser, defaultAction and defaultRequest to use to mutate the request if the user, the action or the resource isn’t valid.

You need to update the Settings type with the new fields:

// Settings defines the settings of the policy.
type Settings struct {
    ValidUsers      []string `json:"validUsers"`
    ValidActions    []string `json:"validActions"`
    ValidResources  []string `json:"validResources"`
    DefaultUser     string   `json:"defaultUser"`
    DefaultAction   string   `json:"defaultAction"`
    DefaultResource string   `json:"defaultResource"`
}

// Valid returns true if the settings are valid.
func (s *Settings) Valid() (bool, error) {
    if len(s.ValidUsers) == 0 {
        return false, fmt.Errorf("validUsers cannot be empty")
    }

    if len(s.ValidActions) == 0 {
        return false, fmt.Errorf("validActions cannot be empty")
    }

    if len(s.ValidResources) == 0 {
        return false, fmt.Errorf("validResources cannot be empty")
    }

    if s.DefaultUser == "" {
        return false, fmt.Errorf("defaultUser cannot be empty")
    }

    if s.DefaultAction == "" {
        return false, fmt.Errorf("defaultUser cannot be empty")
    }

    if s.DefaultResource == "" {
        return false, fmt.Errorf("defaultResource cannot be empty")
    }

    return true, nil
}

Also, the validate function to introduce the mutation:

func validate(payload []byte) ([]byte, error) {
    // Unmarshal the payload into a RawValidationRequest instance
    validationRequest := RawValidationRequest{}
    err := json.Unmarshal(payload, &validationRequest)
    if err != nil {
        // If the payload is not valid, reject the request
        return kubewarden.RejectRequest(
            kubewarden.Message(err.Error()),
            kubewarden.Code(400))
    }

    request := validationRequest.Request
    settings := validationRequest.Settings

    logger.Info("validating request")

    // Accept the request without mutating it if it is valid
    if slices.Contains(settings.ValidUsers, request.User) &&
        slices.Contains(settings.ValidActions, request.Action) &&
        slices.Contains(settings.ValidResources, request.Resource) {
        return kubewarden.AcceptRequest()
    }

    logger.Info("mutating request")

    // Mutate the request if it is not valid
    if !slices.Contains(settings.ValidUsers, request.User) {
        request.User = settings.DefaultUser
    }

    if !slices.Contains(settings.ValidActions, request.Action) {
        request.Action = settings.DefaultAction
    }

    if !slices.Contains(settings.ValidResources, request.Resource) {
        request.Resource = settings.DefaultResource
    }

    return kubewarden.MutateRequest(request)
}