policy

A flexible, composable authorization framework for Kit (inspired by Action Policy)

Files

FileDescription
.editorconfigEditor formatting configuration
.gitignoreGit ignore rules for build artifacts and dependencies
.tool-versionsasdf tool versions (Zig, Kit)
LICENSEMIT license file
README.mdThis file
examples/blog-policy.kitBlog authorization example
kit.tomlPackage manifest with metadata, tasks, and lint configuration
src/core.kitCore authorization helpers
src/error.kitAuthorization errors, result types, and failure reasons
src/main.kitPackage root module
src/scope.kitScope, predicate, and pagination helpers
tests/policy.test.kitEnd-to-end policy behavior tests
tests/types.test.kitPolicy type and helper tests

Dependencies

No Kit package dependencies.

Installation

kit add gitlab.com/kit-lang/packages/kit-policy.git

Usage

import Kit.Policy.Core as PolicyCore
import Kit.Policy.Error as PolicyError
import Kit.Policy.Scope as PolicyScope

type Post = {id: Int, author-id: Int, published?: Bool, title: String}
type User = {id: Int, admin?: Bool}
type AuthContext = {user: User}

post-policy = fn(post, ctx, action) =>
  if ctx.user.admin? then
    PolicyCore.allow
  else
    match PolicyCore.resolve-alias action
      | :show -> PolicyCore.allow-if post.published?
      | :update -> PolicyCore.allow-if (ctx.user.id == post.author-id)
      | :destroy -> PolicyCore.allow-if (ctx.user.id == post.author-id)
      | _ -> PolicyCore.no-rule action

post-scope = fn(posts, ctx) =>
  if ctx.user.admin? then
    posts
  else
    posts |> List.filter (fn(post) => post.published?)

main = fn =>
  user = {id: 1, admin?: false}
  ctx = {user: user}
  post = {id: 1, author-id: 1, published?: true, title: "Hello"}
  posts = [post]

  if PolicyCore.can-with? post-policy post ctx :update then
    println "Can update post"
  else
    println "Cannot update post"

  visible-posts = PolicyScope.scope-with post-scope posts ctx
  page = PolicyScope.paginate 1 10 visible-posts
  info = PolicyScope.pagination-info 1 10 visible-posts

  println "Visible posts: ${page}"
  println "Pages: ${info.pages}"

  err = PolicyError.not-authorized "Post" :update "not the author"
  println (PolicyError.message err)

main

API Overview

Policy.Core

Core helpers for policy functions that return Result Bool PolicyError.

PolicyCore.can-with? policy resource context action
PolicyCore.may-with? policy resource context action

PolicyCore.allow
PolicyCore.deny
PolicyCore.allow-if condition
PolicyCore.deny-if condition
PolicyCore.no-rule action
PolicyCore.allow-or-deny condition resource-name reason action

Pre-check helpers return Option Bool: Some true allows, Some false denies, and None continues to the main rule.

PolicyCore.admin-bypass is-admin? ctx
PolicyCore.owner-check is-owner? resource ctx
PolicyCore.first-pre-check [check1, check2, check3]

Action helpers provide common action groups and aliases.

PolicyCore.crud-actions
PolicyCore.read-actions
PolicyCore.write-actions

PolicyCore.read-action? action
PolicyCore.write-action? action

PolicyCore.resolve-alias :new     # :create
PolicyCore.resolve-alias :edit    # :update
PolicyCore.resolve-alias :delete  # :destroy
PolicyCore.resolve-alias :view    # :show

Policy composition helpers combine several authorization results.

PolicyCore.all-allowed? [result1, result2, result3]
PolicyCore.any-allowed? [result1, result2, result3]

Policy.Scope

Scope helpers filter collections before returning data to a caller.

PolicyScope.scope-with scope-fn items ctx
PolicyScope.filter-by predicate items

PolicyScope.is-owned-by? get-owner-id get-user-id item ctx
PolicyScope.is-published? get-published item
PolicyScope.is-in-state? get-state target-state item
PolicyScope.is-admin-or? check-admin fallback-check ctx item

Predicate combinators are useful for building reusable scope checks.

PolicyScope.both? pred1 pred2 item
PolicyScope.either? pred1 pred2 item
PolicyScope.not-matching? pred item

Pagination helpers are 1-indexed.

page1 = PolicyScope.paginate 1 10 items
pages = PolicyScope.total-pages 10 items
info = PolicyScope.pagination-info 1 10 items

Policy.Error

Error and result types for authorization failures.

type PolicyError =
  | NotAuthorized {resource: String, action: Keyword, reason: String}
  | RuleNotFound {action: Keyword}
  | ContextMissing {field: String}
  | PolicyNotFound {resource-type: String}
  | CustomError String

type FailureReason = FailureReason {
  policy: String, 
  action: Keyword, 
  details: String
}

type AuthResult =
  | Allowed
  | Denied String

Helper functions are exported from the module, so when imported as PolicyError they are called as module functions.

PolicyError.not-authorized resource action reason
PolicyError.rule-not-found action
PolicyError.context-missing field
PolicyError.policy-not-found resource-type
PolicyError.custom message

PolicyError.message err
PolicyError.kind err
PolicyError.is-not-authorized? err
PolicyError.is-rule-not-found? err

PolicyError.new policy action
PolicyError.with-details policy action details
PolicyError.policy reason
PolicyError.action reason
PolicyError.details reason
PolicyError.format reason

PolicyError.allowed
PolicyError.denied reason
PolicyError.is-allowed? result
PolicyError.is-denied? result
PolicyError.reason result
PolicyError.to-result resource-name action result

Design Notes

  • Policies are plain functions, so they are easy to test and compose.
  • Authorization is explicit: helpers return Result Bool PolicyError instead of throwing exceptions.
  • Scopes are separate from policy checks so list filtering can happen before rendering or serialization.
  • Common actions use keywords such as :index, :show, :create, :update, and :destroy.
  • The package is framework-agnostic and can be used with any Kit application code.

Development

Running Examples

Run the blog policy example with the interpreter:

kit run examples/blog-policy.kit

Compile the example to a native binary:

kit build examples/blog-policy.kit && ./blog-policy

Running Tests

Run the test suite:

kit test

Run the test suite with coverage:

kit test --coverage

Running kit dev

Run the standard development workflow (format, check, test):

kit dev

This will:

  1. Format and check source files in src/
  2. Type check examples in examples/
  3. Run tests in tests/ with coverage

Checking Interpreter/Compiler Parity

Run parity checks for examples:

kit parity --failures-only

Use --no-spinner in automation or logs:

kit parity --no-spinner --failures-only

Generating Documentation

Generate API documentation from doc comments:

kit doc

Note: Kit sources with doc comments (##) will generate HTML documents in docs/*.html.

Cleaning Build Artifacts

Remove generated files, caches, and build artifacts:

kit task clean

Note: Defined in kit.toml.

Local Installation

To install this package locally for development:

kit install

This installs the package to ~/.kit/packages/@kit/policy/, making it available for import as Kit.Policy in other projects.

License

This package is released under the MIT License - see LICENSE for details.

Exported Functions & Types

scope-with

Apply a scope function to a collection

(collection -> context -> collection) -> collection -> context -> collection

filter-by

Filter a list using a predicate

(a -> Bool) -> List a -> List a

is-owned-by?

Check if item is owned by user (using extractor functions)

(a -> Int) -> (context -> Int) -> a -> context -> Bool

is-published?

Check if item is published (using extractor function)

(a -> Bool) -> a -> Bool

is-in-state?

Check if item is in a specific state (using extractor function)

(a -> Keyword) -> Keyword -> a -> Bool

is-admin-or?

Check with admin bypass (shows all if admin)

(context -> Bool) -> (a -> Bool) -> context -> a -> Bool

both?

Combine two predicates with AND

(a -> Bool) -> (a -> Bool) -> a -> Bool

either?

Combine two predicates with OR

(a -> Bool) -> (a -> Bool) -> a -> Bool

not-matching?

Negate a predicate

(a -> Bool) -> a -> Bool

paginate

Paginate a list (1-indexed pages)

Int -> Int -> List a -> List a

total-pages

Get total pages

Int -> List a -> Int

pagination-info

Create pagination info

Int -> Int -> List a -> {page: Int, per-page: Int, total: Int, pages: Int}

PolicyError

Authorization error with detailed failure information

Variants

NotAuthorized {resource, action, reason}
RuleNotFound {action}
ContextMissing {field}
PolicyNotFound {resource-type}
CustomError {String}

not-authorized

Create a not-authorized error

String -> Keyword -> String -> PolicyError

rule-not-found

Create a rule-not-found error

Keyword -> PolicyError

context-missing

Create a context-missing error

String -> PolicyError

policy-not-found

Create a policy-not-found error

String -> PolicyError

custom

Create a custom error

String -> PolicyError

message

Get human-readable message from error

PolicyError -> String

kind

Get error kind as keyword

PolicyError -> Keyword

is-not-authorized?

Check if error is not-authorized

PolicyError -> Bool

is-rule-not-found?

Check if error is rule-not-found

PolicyError -> Bool

FailureReason

A failure reason with source policy and action

Variants

FailureReason {policy, action, details}

new

Create a failure reason

String -> Keyword -> FailureReason

with-details

Create a failure reason with details

String -> Keyword -> String -> FailureReason

policy

Get the policy name from a failure reason

FailureReason -> String

action

Get the action from a failure reason

FailureReason -> Keyword

details

Get details string from a failure reason

FailureReason -> String

format

Format failure reason as string

FailureReason -> String

AuthResult

Authorization result type

Variants

Allowed
Denied {String}

allowed

Create an allowed result

AuthResult

denied

Create a denied result with a reason

String -> AuthResult

is-allowed?

Check if result is allowed

AuthResult -> Bool

is-denied?

Check if result is denied

AuthResult -> Bool

reason

Get denial reason (empty string if allowed)

AuthResult -> String

to-result

Convert AuthResult to Result Bool PolicyError

String -> Keyword -> AuthResult -> Result Bool PolicyError

NotAuthorized

Not authorized error variant

RuleNotFound

Rule not found error variant

ContextMissing

Context missing error variant

PolicyNotFound

Policy not found error variant

CustomError

Custom error variant

Allowed

Allowed result variant

Denied

Denied result variant with reason

can-with?

Check authorization using a policy function, returning true/false

(r -> c -> Keyword -> Result Bool e) -> r -> c -> Keyword -> Bool

may-with?

Check authorization using a policy function, returning Option

(r -> c -> Keyword -> Result Bool e) -> r -> c -> Keyword -> Option Bool

admin-bypass

Admin bypass pre-check - allows if user is admin

(context -> Bool) -> context -> Option Bool

owner-check

Owner check pre-check - allows if user owns the resource

(resource -> context -> Bool) -> resource -> context -> Option Bool

first-pre-check

Combine pre-checks (first Some wins)

List (Option Bool) -> Option Bool

crud-actions

CRUD actions list

List Keyword

read-actions

Read-only actions list

List Keyword

write-actions

Write actions list

List Keyword

read-action?

Check if action is a read action

Keyword -> Bool

write-action?

Check if action is a write action

Keyword -> Bool

resolve-alias

Map aliased actions to canonical actions

Keyword -> Keyword

all-allowed?

Check if all results allow

List (Result Bool e) -> Result Bool e

any-allowed?

Check if any result allows

List (Result Bool e) -> Result Bool e

allow

Simple allow rule

Result Bool e

deny

Simple deny rule

Result Bool e

no-rule

Rule-not-found error result

Keyword -> Result Bool PolicyError

allow-if

Conditionally allow based on predicate

Bool -> Result Bool e

deny-if

Conditionally deny based on predicate

Bool -> Result Bool e

allow-or-deny

Allow with custom reason on denial

Bool -> String -> String -> Keyword -> Result Bool PolicyError