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.kitExample: blog policy
kit.tomlPackage manifest with metadata and dependencies
src/core.kitCore module
src/error.kitAuthorization error with detailed failure information
src/main.kitMain module
src/scope.kitApply a scope function to a collection
tests/policy.test.kitTests for policy
tests/types.test.kitTests for types

Dependencies

None - kit-policy has no external dependencies.

Installation

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

Quick Start

import Kit.Policy.Core as PolicyCore

# Define your domain types
type Post = {id: Int, author-id: Int, published?: Bool}
type User = {id: Int, admin?: Bool}
type AuthContext = {user: User}

# Define a policy function
post-policy = fn(post, ctx, action) =>
  if ctx.user.admin? then
    PolicyCore.allow
  else
    match 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

# Check authorization
main = fn =>
  user = {id: 1, admin?: false}
  ctx = {user: user}
  post = {id: 1, author-id: 1, published?: true}

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

Modules

Policy.Core

Core authorization helpers for building policy functions.

Authorization Checks

# Check if action is allowed (returns Bool)
PolicyCore.can-with? policy resource context action

# Check if action is allowed (returns Option Bool)
PolicyCore.may-with? policy resource context action

Rule Builders

# Simple allow/deny
PolicyCore.allow        # Ok true
PolicyCore.deny         # Ok false

# Conditional rules
PolicyCore.allow-if condition    # Ok condition
PolicyCore.deny-if condition     # Ok (not condition)

# Rule not found error
PolicyCore.no-rule action        # Err (RuleNotFound {action: action})

# Allow with custom denial reason
PolicyCore.allow-or-deny condition resource-name reason action

Pre-check Helpers

Pre-checks return Option Bool: Some true to allow, Some false to deny, None to continue to main rule.

# Admin bypass - allows if user is admin
PolicyCore.admin-bypass is-admin? ctx

# Owner check - allows if user owns the resource
PolicyCore.owner-check is-owner? resource ctx

# Combine pre-checks (first Some wins)
PolicyCore.first-pre-check [check1, check2, check3]

Action Helpers

# Action lists
PolicyCore.crud-actions   # [:index, :show, :create, :update, :destroy]
PolicyCore.read-actions   # [:index, :show]
PolicyCore.write-actions  # [:create, :update, :destroy]

# Action predicates
PolicyCore.read-action? action    # true for :index, :show
PolicyCore.write-action? action   # true for :create, :update, :destroy

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

Policy Composition

# All must allow
PolicyCore.all-allowed? [result1, result2, result3]

# Any must allow
PolicyCore.any-allowed? [result1, result2, result3]

Policy.Scope

Helpers for filtering collections based on authorization context.

import Kit.Policy.Scope as PolicyScope

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

visible-posts = PolicyScope.scope-with post-scope all-posts ctx

Pagination

# Paginate a list (1-indexed pages)
page1 = PolicyScope.paginate 1 10 items  # First 10 items
page2 = PolicyScope.paginate 2 10 items  # Items 11-20

# Get pagination info
info = PolicyScope.pagination-info 1 10 items
# Returns: {page: 1, per-page: 10, total: 100, pages: 10}

# Get total pages
pages = PolicyScope.total-pages 10 items

Scope Helpers

# Filter by predicate
PolicyScope.filter-by predicate items

# Predicate combinators
PolicyScope.both? pred1 pred2 item      # pred1 AND pred2
PolicyScope.either? pred1 pred2 item    # pred1 OR pred2
PolicyScope.not-matching? pred item     # NOT pred

Policy.Error

Error types for authorization failures.

import Kit.Policy.Error as PolicyError

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

# Authorization result type
type AuthResult =
  | Allowed
  | Denied String

# Failure reason tracking
type FailureReason = FailureReason {
  policy: String, 
  action: Keyword, 
  details: String
}

Design Philosophy

  • Framework-agnostic: Works with any Kit application, not tied to a specific web framework
  • Functional: Policies are pure functions, easy to test and compose
  • Type-safe: Leverages Kit's type system for compile-time safety
  • Explicit: Results are Result Bool PolicyError, not exceptions
  • Composable: Pre-checks, scopes, and policies can be combined

Examples

See the examples/ directory for complete examples:

  • blog-policy.kit - Blog application with posts and authorization

Running Tests

kit test

License

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}

FailureReason

A failure reason with source policy and action

Variants

FailureReason {policy, action, details}

AuthResult

Authorization result type

Variants

Allowed
Denied {String}

PolicyError

Authorization error with detailed failure information

FailureReason

Failure reason with source policy and action

AuthResult

Authorization result type (Allowed or Denied)

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