ZRB Toolkit · Hands-on Tutorial

Building Enterprise Systems with the ZRB Methodology and Toolkit

A guided, code-first tutorial that walks you through designing a ZRB model and implementing it in Django and Flask — from YAML configuration to protected views.

Author Harris Wang
Version v0.1.0
Reading Time ~45 min
Code Included
Part 01

Introduction

What you will build and why ZRB changes the way we think about enterprise systems.

Welcome! In this tutorial you will learn how to design and implement a secure, maintainable enterprise information system using the Zoned Role-Based (ZRB) framework and its Python toolkit. You will build a simplified University Course Management System step by step, and by the end you will be able to apply the same principles to any real-world domain.

The ZRB approach flips traditional access control on its head: instead of adding security later, you model your organisation's structure (departments, teams, projects) as a zone tree. Within each zone you define roles, assign operations (permissions), and let inheritance (both within a zone and between zones) reduce redundancy. Constraints (separation of duty, time-based, attribute-based) give you fine-grained control.

The ZRB toolkit provides:

  • A storage layer (SQLAlchemy) to persist your configuration.
  • An access control engine that computes effective permissions and evaluates constraints efficiently.
  • Web framework integrations (Django and Flask) with simple decorators to protect your views.
  • A command-line interface to manage configurations.

Let's dive in.

Part 02

Prerequisites

What you need to have installed before starting.

  • Python 3.9 or higher.
  • Basic knowledge of Python and either Django or Flask (we will show both).
  • Familiarity with YAML (helpful but not essential).

For the latest development version, clone the repository and install with pip:

git clone https://github.com/hongxueharriswang/zrb-toolkit.git
cd zrb-toolkit
pip install .

Verify the installation by importing the package:

import zrb
print(zrb.__version__)   # should print 0.1.0 or later

For Django projects, also install Django:

pip install django

For Flask projects, install Flask and Flask-Login:

pip install flask flask-login
Part 03

Understanding ZRB Core Concepts

Before coding, let's get comfortable with the main building blocks.

3.1 Zones #

A zone represents an organisational unit. Zones form a tree — the root is the entire enterprise, children are subdivisions. For a university:

University (root)
├── Faculty of Engineering
│   ├── Computer Science
│   └── Electrical Engineering
└── Faculty of Arts
    ├── History
    └── Philosophy

Each zone has an id (unique) and a parent_id (null for root).

3.2 Operations #

An operation is a single protected action, e.g., grade:submit, course:enroll. Operations are identified by a string and belong to an application (like grading or curriculum).

3.3 Roles #

A role is a job function within a specific zone. Each role can have base permissions — a set of operation IDs directly granted. Roles can have a parent role inside the same zone, creating an intra-zone hierarchy: a senior role inherits all base permissions of its junior roles.

3.4 Gamma Mappings #

Sometimes a role in a child zone should inherit from a role in its parent zone. This is inter-zone inheritance and is defined by a gamma mapping. A gamma mapping specifies child_zone_id, child_role_id, parent_zone_id, parent_role_id, and optional weight and priority.

3.5 Users and Assignments #

A user is an individual with an id (matching your authentication system). Users are assigned to roles within specific zones via assignments.

3.6 Constraints #

Constraints are rules that can deny access even if the user has the permission. Types:

  • Separation of duty (SoD): e.g., a person cannot approve their own purchase order.
  • Temporal: e.g., grade submission only allowed during exam week.
  • Attribute: e.g., reorder only if stock < 10.
  • Context: e.g., test runs allowed only in QA environment.

3.7 Access Modes #

  • Direct (n_rzbac): only base permissions of the assigned roles are considered. Inheritance is ignored.
  • Inferential (i_rzbac): full effective permissions, including all inheritance. Default for most operations.
Part 04

Designing the University System

We'll design a system with two faculties and two departments each.

Step 1: Zone Tree

University (root)
├── Faculty of Engineering (eng)
│   ├── Computer Science (cs)
│   └── Electrical Engineering (ee)
└── Faculty of Arts (arts)
    ├── History (hist)
    └── Philosophy (phil)

Step 2: Operations

  • grade:view — view own grades
  • grade:submit — submit final grades (professors only)
  • grade:enter — enter grades as a TA
  • course:enroll — students enroll
  • course:view — view course list
  • course:create — create a new course (deans only)
  • faculty:report — view faculty report (deans)
  • schedule:manage — manage schedule (department heads)
  • prof:assign — assign professors to courses (department heads)
  • user:manage, zone:manage — admin operations

Step 3: Roles per Zone

  • University root: admin (with user:manage, zone:manage)
  • Faculty of Engineering: dean_eng, prof_eng, student_eng, ta_eng
  • Computer Science: head_cs, prof_cs, student_cs, ta_cs

Step 4: Intra-zone Hierarchies

  • dean_eng inherits from prof_eng
  • prof_eng inherits from ta_eng
  • ta_eng inherits from student_eng

Step 5: Gamma Mappings

  • (cs, prof_cs) → (eng, prof_eng)
  • (cs, student_cs) → (eng, student_eng)
  • (cs, ta_cs) → (eng, ta_eng)

Step 6: Constraints

  • SoD: A student cannot be a TA in the same course.
  • Temporal: Grade submission only allowed between May 1 and June 15, 2025.

Step 7: Users and Assignments

  • alice — professor and head of CS
  • bob — CS student
  • charlie — CS TA
  • dave — Dean of Engineering
  • eve — Arts professor
Part 05

Writing the ZRB Configuration (YAML)

Create a file named university_config.yaml.

zones:
  - id: root
    name: University
  - id: eng
    name: Faculty of Engineering
    parent_id: root
  - id: cs
    name: Computer Science
    parent_id: eng
  - id: ee
    name: Electrical Engineering
    parent_id: eng
  - id: arts
    name: Faculty of Arts
    parent_id: root
  - id: hist
    name: History
    parent_id: arts
  - id: phil
    name: Philosophy
    parent_id: arts

operations:
  - id: user:manage
    app_name: admin
    name: Manage users
  - id: zone:manage
    app_name: admin
    name: Manage zones
  - id: course:create
    app_name: curriculum
    name: Create course
  - id: faculty:report
    app_name: reports
    name: View faculty report
  - id: grade:submit
    app_name: grading
    name: Submit grades
  - id: course:view
    app_name: curriculum
    name: View courses
  - id: grade:view
    app_name: grading
    name: View grades
  - id: course:enroll
    app_name: enrollment
    name: Enroll in course
  - id: grade:enter
    app_name: grading
    name: Enter grades (TA)
  - id: schedule:manage
    app_name: schedule
    name: Manage schedule
  - id: prof:assign
    app_name: faculty
    name: Assign professor

roles:
  - id: admin
    zone_id: root
    name: Admin
    base_permissions: ["user:manage", "zone:manage"]

  - id: dean_eng
    zone_id: eng
    name: Dean
    base_permissions: ["course:create", "faculty:report"]
    parent_role_id: prof_eng
  - id: prof_eng
    zone_id: eng
    name: Professor
    base_permissions: ["grade:submit", "course:view"]
    parent_role_id: ta_eng
  - id: ta_eng
    zone_id: eng
    name: TA
    base_permissions: ["grade:enter"]
    parent_role_id: student_eng
  - id: student_eng
    zone_id: eng
    name: Student
    base_permissions: ["grade:view", "course:enroll"]

  - id: head_cs
    zone_id: cs
    name: DeptHead
    base_permissions: ["schedule:manage", "prof:assign"]
    parent_role_id: prof_cs
  - id: prof_cs
    zone_id: cs
    name: Professor
    base_permissions: []
  - id: ta_cs
    zone_id: cs
    name: TA
    base_permissions: []
  - id: student_cs
    zone_id: cs
    name: Student
    base_permissions: []

gamma_mappings:
  - child_zone_id: cs
    child_role_id: prof_cs
    parent_zone_id: eng
    parent_role_id: prof_eng
    priority: 1
  - child_zone_id: cs
    child_role_id: student_cs
    parent_zone_id: eng
    parent_role_id: student_eng
    priority: 1
  - child_zone_id: cs
    child_role_id: ta_cs
    parent_zone_id: eng
    parent_role_id: ta_eng
    priority: 1

constraints:
  - id: sod_student_ta
    type: separation_of_duty
    target: { operation_id: "grade:enter" }
    condition: { cannot_have_role: "student" }
    is_negative: true
    priority: 10
  - id: temporal_grade_submit
    type: temporal
    target: { operation_id: "grade:submit" }
    condition: { time_range: ["2025-05-01T00:00", "2025-06-15T23:59"] }
    is_negative: false
    priority: 10

users:
  - id: u1
    username: alice
    email: alice@uni.edu
    attributes: {}
  - id: u2
    username: bob
    email: bob@uni.edu
    attributes: {}
  - id: u3
    username: charlie
    email: charlie@uni.edu
    attributes: {}
  - id: u4
    username: dave
    email: dave@uni.edu
    attributes: {}
  - id: u5
    username: eve
    email: eve@uni.edu
    attributes: {}

assignments:
  - user_id: u1
    zone_id: cs
    role_id: prof_cs
  - user_id: u1
    zone_id: cs
    role_id: head_cs
  - user_id: u2
    zone_id: cs
    role_id: student_cs
  - user_id: u3
    zone_id: cs
    role_id: ta_cs
  - user_id: u4
    zone_id: eng
    role_id: dean_eng
  - user_id: u5
    zone_id: arts
    role_id: prof_arts
Part 06

Loading the Configuration

Write a small Python script to load the YAML into the ZRB database.

import yaml
from zrb.storage.sqlalchemy import SQLAlchemyStore
from zrb.core.models import User, Zone, Role, Operation, GammaMapping, Constraint, UserZoneRole
from zrb.core.types import ConstraintType

def load_config(yaml_file, db_url='sqlite:///zrb.db'):
    store = SQLAlchemyStore(db_url)
    store.create_all()

    with open(yaml_file) as f:
        config = yaml.safe_load(f)

    for z in config['zones']:
        zone = Zone(**z)
        store.create_zone(zone)
        print(f"Zone: {zone.name}")

    for o in config['operations']:
        op = Operation(**o)
        store.create_operation(op)
        print(f"Operation: {op.name}")

    for r in config['roles']:
        role = Role(
            id=r['id'],
            zone_id=r['zone_id'],
            name=r['name'],
            parent_role_id=r.get('parent_role_id'),
            description=r.get('description', ''),
            base_permissions=set(r.get('base_permissions', []))
        )
        store.create_role(role)
        print(f"Role: {role.name} in zone {role.zone_id}")

    for g in config.get('gamma_mappings', []):
        gamma = GammaMapping(**g)
        store.create_gamma_mapping(gamma)
        print(f"Gamma: {gamma.child_role_id} -> {gamma.parent_role_id}")

    for c in config.get('constraints', []):
        c['type'] = ConstraintType(c['type'])
        constraint = Constraint(**c)
        store.create_constraint(constraint)
        print(f"Constraint: {constraint.id}")

    for u in config.get('users', []):
        user = User(**u)
        store.create_user(user)
        print(f"User: {user.username}")

    for a in config.get('assignments', []):
        uzr = UserZoneRole(**a)
        store.assign_user_to_role(uzr.user_id, uzr.zone_id, uzr.role_id)
        print(f"Assignment: user {uzr.user_id} -> role {uzr.role_id} in zone {uzr.zone_id}")

    print("Configuration loaded successfully.")

if __name__ == '__main__':
    load_config('university_config.yaml')

Run it:

python load_config.py

You should see output confirming each entity was created. The database file zrb.db now contains your ZRB configuration.

Part 07

Building the Web Application — Django

Create a Django project that uses the ZRB database and protects its views with decorators.

7.1 Create Django Project and App #

django-admin startproject university_project
cd university_project
python manage.py startapp university

7.2 Create the Zone Middleware #

In university/middleware.py:

from django.conf import settings
from zrb.storage.sqlalchemy import SQLAlchemyStore

zrb_store = SQLAlchemyStore(settings.ZRB_DATABASE_URL)

class ZoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        host = request.get_host().split(':')[0]
        subdomain = host.split('.')[0] if '.' in host else None
        zone_map = {
            'cs': 'cs',
            'ee': 'ee',
            'eng': 'eng',
            'hist': 'hist',
            'phil': 'phil',
            'arts': 'arts',
            None: 'root',
        }
        zone_id = zone_map.get(subdomain, 'root')
        request.zone = zrb_store.get_zone(zone_id)
        return self.get_response(request)

7.3 Create the Access Engine Singleton #

In university/zrb_setup.py:

from django.conf import settings
from zrb.storage.sqlalchemy import SQLAlchemyStore
from zrb.engine.access import AccessEngine

store = SQLAlchemyStore(settings.ZRB_DATABASE_URL)
engine = AccessEngine(store)

import zrb.web.django
zrb.web.django.engine = engine
zrb.web.django.store = store

7.4 Define Business Models #

In university/models.py:

from django.db import models
from django.contrib.auth.models import User

class Course(models.Model):
    code = models.CharField(max_length=10, unique=True)
    name = models.CharField(max_length=100)
    credits = models.IntegerField()
    zone_id = models.CharField(max_length=50)

    def __str__(self):
        return f"{self.code}: {self.name}"

class Enrollment(models.Model):
    student = models.ForeignKey(User, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    enrolled_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('student', 'course')

class Grade(models.Model):
    student = models.ForeignKey(User, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    grade = models.CharField(max_length=2)
    submitted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='grades_submitted')
    submitted_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('student', 'course')

Run migrations:

python manage.py makemigrations university
python manage.py migrate

7.5 Create Protected Views #

In university/views.py:

import json
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from zrb.web.django import i_rzbac, n_rzbac
from .models import Course, Enrollment, Grade
from .zrb_setup import store, engine

@login_required
@i_rzbac(operation='grade:view')
def view_grades(request):
    grades = Grade.objects.filter(student=request.user).values('course__code', 'grade')
    return JsonResponse(list(grades), safe=False)

@login_required
@csrf_exempt
@n_rzbac(operation='grade:submit')
def submit_grade(request):
    if request.method != 'POST':
        return JsonResponse({'error': 'Method not allowed'}, status=405)
    data = json.loads(request.body)
    grade = Grade.objects.create(
        student_id=data['student_id'],
        course_id=data['course_id'],
        grade=data['grade'],
        submitted_by=request.user
    )
    return JsonResponse({'status': 'submitted', 'id': grade.id})

@login_required
@csrf_exempt
@i_rzbac(operation='course:enroll')
def enroll(request):
    if request.method != 'POST':
        return JsonResponse({'error': 'Method not allowed'}, status=405)
    data = json.loads(request.body)
    course = Course.objects.get(id=data['course_id'])
    enrollment, created = Enrollment.objects.get_or_create(student=request.user, course=course)
    return JsonResponse({'status': 'enrolled' if created else 'already enrolled'})

@login_required
@i_rzbac(operation='course:view')
def list_courses(request):
    zone_id = request.zone.id if request.zone else 'root'
    courses = Course.objects.filter(zone_id=zone_id).values('id', 'code', 'name')
    return JsonResponse(list(courses), safe=False)

7.6 Wire Up URLs and Login #

In university/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('grades/', views.view_grades, name='view_grades'),
    path('grades/submit/', views.submit_grade, name='submit_grade'),
    path('enroll/', views.enroll, name='enroll'),
    path('courses/', views.list_courses, name='list_courses'),
]
User ID alignment: The ZRB users must match Django's user IDs. In our config, user IDs are u1, u2, etc., but Django's User model uses integer primary keys. For simplicity, we assume you manually create Django users with IDs 1, 2, 3, ... and update the ZRB config to use those numeric IDs (as strings).

Run the development server:

python manage.py runserver
Part 08

Building the Web Application — Flask

If you prefer Flask, here is the equivalent.

import os
import yaml
from flask import Flask, request, jsonify, abort
from flask_login import LoginManager, UserMixin, login_user, current_user, login_required
from zrb.storage.sqlalchemy import SQLAlchemyStore
from zrb.engine.access import AccessEngine
from zrb.web.flask import ZRBFlask
from zrb.core.models import User as ZRBUser

app = Flask(__name__)
app.secret_key = 'dev-secret-key'

db_url = 'sqlite:///zrb.db'
store = SQLAlchemyStore(db_url)
engine = AccessEngine(store)

zrb = ZRBFlask(app, engine)

login_manager = LoginManager()
login_manager.init_app(app)

class User(UserMixin):
    def __init__(self, id, username):
        self.id = id
        self.username = username

users_db = {
    '1': {'username': 'alice', 'zrb_id': 'u1'},
    '2': {'username': 'bob', 'zrb_id': 'u2'},
}

@login_manager.user_loader
def load_user(user_id):
    if user_id in users_db:
        return User(user_id, users_db[user_id]['username'])
    return None

@app.route('/login/')
def login(user_id):
    if user_id not in users_db:
        abort(404)
    user = User(user_id, users_db[user_id]['username'])
    login_user(user)
    return f"Logged in as {user.username}"

def zone_from_request():
    host = request.host.split(':')[0]
    subdomain = host.split('.')[0] if '.' in host else None
    zone_map = {
        'cs': 'cs',
        'eng': 'eng',
        None: 'root',
    }
    zone_id = zone_map.get(subdomain, 'root')
    return store.get_zone(zone_id)

zrb.zone_resolver = zone_from_request

@app.route('/api/grades')
@login_required
@zrb.i_rzbac(operation='grade:view')
def view_grades():
    return jsonify({'grades': []})

@app.route('/api/grades/submit', methods=['POST'])
@login_required
@zrb.n_rzbac(operation='grade:submit')
def submit_grade():
    return jsonify({'status': 'submitted'})

@app.route('/api/enroll', methods=['POST'])
@login_required
@zrb.i_rzbac(operation='course:enroll')
def enroll():
    return jsonify({'status': 'enrolled'})

@app.route('/api/courses')
@login_required
@zrb.i_rzbac(operation='course:view')
def list_courses():
    return jsonify({'courses': []})

if __name__ == '__main__':
    app.run(debug=True)
Part 09

Handling Constraints with Context

To evaluate a temporal constraint, the engine needs the current time.

The decorators do not automatically pass context. You have two choices:

Option A: Direct Engine Call

from .zrb_setup import store, engine

def submit_grade(request):
    data = json.loads(request.body)
    context = {'current_time': datetime.now().isoformat()}
    user = store.get_user(str(request.user.id))
    op = store.get_operation('grade:submit')
    zone = request.zone
    if not engine.decide(user, op, zone, mode='inferential', context=context):
        return HttpResponseForbidden()
    # ... proceed

Option B: Custom Decorator with Context

def i_rzbac_with_context(operation=None, context_func=None):
    def decorator(view):
        @wraps(view)
        def _wrapped(request, *args, **kwargs):
            context = context_func(request) if context_func else {}
            user = store.get_user(str(request.user.id))
            op = store.get_operation(operation or request.resolver_match.url_name)
            zone = request.zone
            if not engine.decide(user, op, zone, mode='inferential', context=context):
                return HttpResponseForbidden()
            return view(request, *args, **kwargs)
        return _wrapped
    return decorator
Part 10

Testing and Simulation

Write unit tests using the engine directly.

import pytest
from zrb.storage.sqlalchemy import SQLAlchemyStore
from zrb.engine.access import AccessEngine

@pytest.fixture
def store():
    store = SQLAlchemyStore('sqlite:///:memory:')
    store.create_all()
    yield store

def test_professor_can_submit_grades(store):
    engine = AccessEngine(store)
    user = store.get_user('u1')
    zone = store.get_zone('cs')
    op = store.get_operation('grade:submit')
    assert engine.decide(user, op, zone) is True
Part 11

Deployment Considerations

Moving from development to production.

  • Use a production database (PostgreSQL) for the ZRB store.
  • Set up proper caching (Redis) if needed.
  • Run your Django/Flask app with Gunicorn behind Nginx.
  • Configure subdomains correctly in DNS and in your middleware.
  • Regularly back up the ZRB configuration (YAML) and database.
Tip: For a production Django deployment, run Gunicorn with multiple workers, and configure Nginx as a reverse proxy with proper subdomain routing. Remember to set DEBUG = False and configure ALLOWED_HOSTS.
Part 12

Conclusion

You have built a complete, secure enterprise system using ZRB.

You have learned how to:

  • Model an organisation as a zone tree.
  • Define roles, operations, and inheritance.
  • Enforce constraints.
  • Integrate with Django and Flask.
  • Test and simulate access decisions.

The same approach scales to any domain — hospital, banking, e-commerce, project management. The ZRB toolkit gives you a solid foundation to build systems that are secure by design and easy to maintain.

Next steps: For a deeper reference covering every module, class, and method, read the ZRB User Guide. Explore the example systems in the ZRB Toolkit repository.
👤