Scopie
Scopie is a small, explicit, scope-based authorization engine.
It evaluates hierarchical permission patterns against requested actions using a deterministic, spec-defined algorithm. Scopie is designed to be embedded directly into applications and services, without requiring a policy server, DSL, or external data source.
Scopie follows clarity and explicit behavior over flexibility.
Status
Scopie is currently in alpha.
Behavior is versioned and defined by the scenarios.json file.
Implementations are expected to conform exactly to the scenarios for their supported version.
What Scopie Is (and Is Not)
Scopie is:
- A deterministic scope evaluator
- Based on hierarchical paths, wildcards, arrays, and variables
- Explicit allow vs deny semantics
- Langauge agnostic via a shared specification
- Easy to reason about and test
- ??? data owned by you
Scopie is not:
- A policy engine
- A role management system
- An attribute-based authorization framework
- A centralized policy service
If you need expressive policy languages, dynamic external lookups, or centralized policy distribution, Scopie is likely not the right tool.
Core Concepts
Permissions and Actions
A permission that would allow or deny access:
An action is a concrete request being evaluated:
Scopie evaluates an action against a set of granted permissions.
Hierarchy and Wildcards
Permissions are hierarchical and path-based.
*matches exactly one path segment**matches one or more path segments- Arrays (
|) match one of several values
Examples:
Variables
Permissions may include variables that are substituted at evaluation time:
Variables are simple substitutions, not expressions or conditions.
Evaluation Model
Scopie's evaluation rules are intentionally simple and explicit.
Given a set of permissions and an action:
- Permissions are evaluated in sequence.
- If a matching deny permission is found, evaluation immediately stops and access is denied.
- If a matching allow permission is found, it is recorded and evaluation continues only for deny permissions.
- If evaluation completes with at least one matching allow and no matching deny, access is allowed.
- If no permissions match, access is denied.
This short-circuit behavior is deterministic and does not affect the final result:
- Deny always overrides allow
- Permission order does not change outcomes
Specification and Scenarios
Scopie is spec-first.
The scenarios.json defines the normative behavior of Scopie across:
- Allow and deny precedence
- Wildcard and super-wildcard semantics
- Variable substitution
- Invalid input handling
- Determinism and order invariance
- A cross-language conformance suite
All Scopie implementations must conform to these scenarios. If you want to understand exactly how Scopie behaves, then this is the place to look.
Example
A portion of our application is around building, running and responding to financial reports. We run half, quarterly, monthly and weekly reports.
Verbs
- Edit: Modify how the report is built
- Run: Manually run the report
- Read: Read the reports in our tool
- Approve: Sign off on the report, approved reports would then be shared
- Delete: Remove an invalid or broken report
Format
For the above reasons we are going to specify our permissions and actions as:
We could expand this later to include some sort of organization or business group but for this example, we will keep it simple.
Users
- Maya is allowed to do everything ( the boss )
- Adam is allowed to edit and read any report ( makes changes to the queries )
- Tyler can only read reports ( reviews reports but doesn't need to approve them )
- Elisa can do everything but delete ( mostly there to approve, but occasionally edits queries )
- Jenna can edit and read but only the weekly reports ( a new hire working up )
Quick Intro
See implementations for more complete guides per language.
import { isAllowed } from "scopie";
const users = {
elsa: {
permissions: ["allow:blog/create|update"],
},
bella: {
permissions: ["allow:blog/create"],
},
]
const blogPosts = {}
function createBlog(username, blogSlug, blogContent) {
const user = users[username]
if (isAllowed(["blog/create"], user.permissions)) {
blogPosts[blogSlug] = {
author: user,
content: blogContent,
}
}
}
function updateBlog(username, blogSlug, blogContent) {
const user = users[username]
if (isAllowed(["blog/update"], user.permissions)) {
blogPosts[blogSlug] = {
author: user,
content: blogContent,
}
}
}
import { isAllowed } from "scopie";
type User = {
permissions: Array<string>;
};
type BlogPost = {
author: User;
content: string;
}
type UserStore = {
[key: string]: User
}
type BlogStore = {
[key: string]: BlogPost
}
const users: UserStore = {
elsa: {
permissions: ["allow:blog/create|update"],
},
bella: {
permissions: ["allow:blog/create"],
},
}
const blogPosts: BlogStore = {}
function createBlog(username: string, blogSlug: string, blogContent: string) {
const user = users[username]
if (isAllowed(["blog/create"], user.permissions)) {
blogPosts[blogSlug] = {
author: user,
content: blogContent,
}
}
}
function updateBlog(username: string, blogSlug: string, blogContent: string) {
const user = users[username]
if (isAllowed(["blog/update"], user.permissions)) {
blogPosts[blogSlug] = {
author: user,
content: blogContent,
}
}
}
import (
"errors"
"github.com/miniscruff/scopie-go"
)
type User struct {
Permissions []string
}
type BlogPost struct {
Author User
Content string
}
var userStore map[string]User = map[string]User{
"elsa": User{
Permissions: []string{"allow:blog/create|update"},
},
"belle": User{
Permissions: []string{"allow:blog/create"},
},
}
var blogStore map[string]BlogPost = map[string]BlogPost{}
func createBlog(username, blogSlug, blogContent string) error {
user := users[username]
allowed, err := scopie.IsAllowed([]string{"blog/create"}, user.Permissions, nil)
if err != nil {
return err
}
if !allowed {
return errors.New("not allowed to create a blog post")
}
blogStore[blogSlug] = BlogPost{
Author: user,
Content: blogContent,
}
return nil
}
func updateBlog(username, blogSlug, blogContent string) error {
user := users[username]
allowed, err := scopie.IsAllowed([]string{"blog/update"}, user.Permissions, nil) {
if err != nil {
return err
}
if !allowed {
return errors.New("not allowed to update this blog post")
}
blogPosts[blogSlug] = BlogPost{
author: user,
content: blogContent,
}
return nil
}
from scopie import is_allowed
users = {
"elsa": {
"permissions": ["allow:blog/create|update"],
},
"bella": {
"permissions": ["allow:blog/create"],
},
}
blogPosts = {}
def create_blog(username, blogSlug, blogContent):
user = users[username]
if is_allowed(["blog/create"], user["permissions"]):
blogPosts[blogSlug] = {
"author": user,
"content": blogContent,
}
def update_blog(username, blogSlug, blogContent):
user = users[username]
if is_allowed(["blog/update"], user["permissions"]):
blogPosts[blogSlug] = {
"author": user,
"content": blogContent,
}