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​
Please refer to Introduction to WASI for an overview of the WASI execution mode.
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.
Validation​
Let's 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 usersaction
is in the list of valid actionsresource
is in the list of valid resources
Start by scaffolding the policy by using the go WASI policy template.
First, we need to define the types that represent the payload of the request.
We will declare a custom RawValidationRequest
type that contains the Request
and the Settings
,
instead of using the ValidationRequest
type that is provided by the kw_sdk.go
:
// 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 we need to define the Settings
type and the validateSettings
function in the settings.go
file:
// Settings represents the settings of the policy.
type Settings struct {
ValidUsers []string `json:"validUsers"`
ValidActions []string `json:"validActions"`
ValidResources []string `json:"validResources"`
}
func validateSettings(input []byte) []byte {
var response SettingsValidationResponse
settings := &Settings{}
if err := json.Unmarshal(input, &settings); err != nil {
response = RejectSettings(Message(fmt.Sprintf("cannot unmarshal settings: %v", err)))
} else {
response = validateCliSettings(settings)
}
responseBytes, err := json.Marshal(&response)
if err != nil {
log.Fatalf("canno marshal validation response: %v", err)
}
return responseBytes
}
func validateCliSettings(settings *Settings) SettingsValidationResponse {
if len(settings.ValidUsers) == 0 {
return RejectSettings(Message(
"At least one valid user must be specified"))
}
if len(settings.ValidActions) == 0 {
return RejectSettings(Message(
"At least one valid action must be specified"))
}
if len(settings.ValidResources) == 0 {
return RejectSettings(Message(
"At least one valid resource must be specified"))
}
return AcceptSettings()
}
Finally, we update the validation logic in validate.go
:
func validate(input []byte) []byte {
var validationRequest RawValidationRequest
validationRequest.Settings = Settings{}
decoder := json.NewDecoder(strings.NewReader(string(input)))
decoder.DisallowUnknownFields()
err := decoder.Decode(&validationRequest)
if err != nil {
return marshalValidationResponseOrFail(
RejectRequest(
Message(fmt.Sprintf("Error deserializing validation request: %v", err)),
Code(400)))
}
return marshalValidationResponseOrFail(
validateRequest(validationRequest.Settings, validationRequest.Request))
}
func marshalValidationResponseOrFail(response ValidationResponse) []byte {
responseBytes, err := json.Marshal(&response)
if err != nil {
log.Fatalf("cannot marshal validation response: %v", err)
}
return responseBytes
}
func validateRequest(settings Settings, request Request) ValidationResponse {
if slices.Contains(settings.ValidUsers, request.User) &&
slices.Contains(settings.ValidActions, request.Action) &&
slices.Contains(settings.ValidResources, request.Resource) {
return AcceptRequest()
}
return RejectRequest(
Message("The request cannot be accepted."),
Code(403))
}
Mutation​
Let's modify the previous example to mutate the request instead of rejecting it.
In this case, the settings will contain the defaultUser
, defaultAction
and defaultRequest
that will be used to mutate the request if the user, the action or the resource is not valid.
We need to update the Settings
type with the new fields:
// Settings represents 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"`
}
func validateCliSettings(settings *Settings) SettingsValidationResponse {
if len(settings.ValidUsers) == 0 {
return RejectSettings(Message(
"At least one valid user must be specified"))
}
if len(settings.ValidActions) == 0 {
return RejectSettings(Message(
"At least one valid action must be specified"))
}
if len(settings.ValidResources) == 0 {
return RejectSettings(Message(
"At least one valid resource must be specified"))
}
if settings.DefaultUser == "" {
return RejectSettings(Message(
"Default user must be specified"))
}
if settings.DefaultAction == "" {
return RejectSettings(Message(
"Default action must be specified"))
}
if settings.DefaultResource == "" {
return RejectSettings(Message(
"Default resource must be specified"))
}
return AcceptSettings()
}
We also need to update the ValidationResponse
struct and the MutateRequest
function in kw_sdk.go
to remove the Kubernetes-specific types and use our custom types instead:
// ValidationResponse defines the response given when validating a request
type ValidationResponse struct {
Accepted bool `json:"accepted"`
// Optional - ignored if accepted
Message *string `json:"message,omitempty"`
// Optional - ignored if accepted
Code *uint16 `json:"code,omitempty"`
// Optional - used only by mutating policies
MutatedObject *Request `json:"mutated_object,omitempty"`
}
// MutateRequest accepts the request. The given `mutatedObject` is how
// the evaluated object must look once accepted
func MutateRequest(mutatedObject *Request) ValidationResponse {
return ValidationResponse{
Accepted: true,
MutatedObject: mutatedObject,
}
}
Now we can update the validate
function to mutate the request if it is not valid:
func validateRequest(settings Settings, request Request) ValidationResponse {
if slices.Contains(settings.ValidUsers, request.User) &&
slices.Contains(settings.ValidActions, request.Action) &&
slices.Contains(settings.ValidResources, request.Resource) {
return AcceptRequest()
}
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 MutateRequest(&request)
}