An RBAC Demo: For Aiur

Based on the understanding of RBAC from the previous article, and due to assignment requirements, I will write a simple Demo as a practice of RBAC.

I have no intention of making the Demo excessively large. My goal is to illustrate the concepts, so it remains “small but complete.”

So, what is the goal of this Demo, and what can it do?

Analysis and Design

To make it interesting and fully reflect the problem, I will simulate a greatly simplified scenario in my Demo, where each unit miraculously possesses subjective initiative, set in a StarCraft battle from the perspective of a Protoss player.

A good Demo needs a good name, so this Demo is called: For Aiur! (Aiur is the Protoss homeworld, and every Zealot says this impassioned phrase when warped onto the battlefield: “For Aiur!”)

In For Aiur, you will lead your team to produce enough Zealot warriors to destroy the Destroyer Amon and save the entire universe. If you don’t have enough Zealots, you will fail, the stars will whisper, and all things will vanish.

All of this is embodied in a RESTful service written with Python’s Flask framework.

In a real scenario, every entity except the player could be created, thus they can all be seen as resources. To reflect RBAC, I have added the following constraints:

  • The few entities provided by this Demo are unique and always exist, as if they were one-of-a-kind, thus removing any possibility of entities being recursively created (e.g., a Probe can produce a Nexus, and a Nexus can produce Probes).
  • In reality, Gateways require energy from Pylons to operate, but this is not reflected here. I explain it as our Gateways receiving energy support from the Protoss legendary mothership, the Spear of Adun, in synchronous orbit, thus treating Pylons only as resources providing population cap.

This battle scenario contains the following objects:

Subject

  • Player (you) thrimbda
  • Probe (basic worker unit, can gather production resources and construct buildings) probe
  • Gateway (produces Zealots, a combat unit) gateway

Role

  • Archon (Supreme Commander, allocates all resources) archon
  • Crystal Collector (only Probes can take on this role) crystal_collector
  • Portal for Zealot Warp-in (beacon supporting the teleportation of Protoss warriors) portal
  • Pylon Transporter (with Pylons, we can have enough population cap to warp in Zealots) pylon_transporter

Resource

  • Unmined Crystal (used to build Pylons and warp in Zealots, initial value unknown)
  • Mined Crystal (used to build Pylons and warp in Zealots, initial value 0)
  • Population Cap (provides energy for warping, i.e., what we call supply, initial value 0)
  • Zealot (combat unit, the warriors you use to save the universe, initial value 0)

Permission

Since each operation on a resource is a permission, we do not list operations separately here but directly give the permissions and their descriptions.

  • Mine Crystal (up to 1000 units per collection)
  • Observe Unmined Crystal (observe total amount)
  • Status Report (report the current amount of resources you own)
  • Scout Amon’s Strength (calculate the required number of Zealots)
  • Build Pylon (each Pylon provides 10 population cap units and costs 100 Crystal)
  • Produce Zealot (each Zealot consumes 2 population cap units and 100 Crystal)
  • Attack Amon (Victory or death!)

SA

The many-to-many relationships between Subject-Role and Role-Permission are represented using Python’s tuple data structure, and this is also the case in the implementation. Therefore, this application does not use a database.

subject_role = (('thrimbda', 'archon'),
                ('probe', 'crystal_collector'),
                ('probe', 'pylon_transporter'),
                ('gateway', 'portal'))

PA

role_permission = (('archon', 'get_status'),
                   ('archon', 'for_aiur'),
                   ('archon', 'scout'),
                   ('crystal_collector', 'get_crystal'),
                   ('crystal_collector', 'crystal_status'),
                   ('pylon_transporter', 'get_status'),
                   ('pylon_transporter', 'transport_pylon'),
                   ('portal', 'transport_zealot'),
                   ('portal', 'get_status'))

Implementation

Repository URL

Online Deployment (Slow)

Overview

In summary, I used Python’s Flask framework to write a RESTful-style service. The entire application does not involve a frontend, so there are no security issues like bypassing the frontend.

First, a characteristic of this Demo is that it does not use a database. RBAC does not mandate the use of a database, and using a database in RBAC is an intuitive and natural thing. However, in For Aiur, we do not use a database; instead, we use files to represent the RBAC Subject-Role-Permission relationships. Databases themselves evolved from file systems. Files are used here because the system is simple enough, and to further reduce system complexity for illustrative purposes. The specific file format is as described in the SA and PA relationship sections above.

Regarding RESTful

Here’s a brief mention of RESTful (Representational State Transfer).

As the name suggests, (Resource) Representational State Transfer.

In a web service, the services provided are the system’s resources, represented in the form of URIs, and the form of the service is the operation on the resources (state transfer), represented in the form of HTTP verbs. Several concepts here correspond well to resources and operations in RBAC. Therefore, what I need to do is apply RBAC’s permission management to the operations on resources in REST.

Objects in RBAC

As can be seen in these two configuration files, besides SA and PA, we can implicitly derive S, R, P:

# Derive S and R lists from the above tuple subject_role
subjects = list(set([item[0] for item in subject_role]))
roles = list(set([item[1] for item in subject_role]))

And SE can correspond well to the session in a web application, serving as a temporary object for a subject during a single login:

# The subject uses this to log into the Spear of Adun tactical management system API. Here, session is a global object in Flask, and its implementation details are not elaborated.
class SpearOfAdun(Resource):
    
    def post(self):
        args = self.putparser.parse_args()
        if args['subject'] is not None:
            abortInvalideSubject(args['subject'])
        if args['role'] is not None:
            abortInvalideRole(args['role'])
        checkRole(args['subject'], args['role'], subject_role)
        session['subject'] = args['subject']
        session['role'] = args['role']
        return {'message': 'login as %s using %s' % (session['subject'], session['role'])}, 201

The ultimate purpose of Subject-Role modeling is to isolate and assign permissions, ensuring proper use and protection of system resources.

In For Aiur, I treat permissions as internal properties of the web API, for example:

# API for warping in Zealots
class Zealot(Resource):

    def put(self):
        permission = 'transport_zealot' # Permission
        abortIfSubjectUnauthenticated(session) # Login verification
        checkPermission(session['role'], permission, role_permission) # Check if the subject in this role can request this permission
        args = self.putparser.parse_args()
        amount = nexus.transport(args['amount'])
        return {'message': 'transport %d zealot warriors, En Taro Tassadar!' % amount}, 200

In the two API classes used as examples above, each class exists as a resource in the system, and the provided HTTP methods are operations on the resources.

Thus, all the objects in RBAC are now present.

Business Logic

Since For Aiur is a real, playable online real-time strategy game API, it’s necessary to explain its business logic:

The player’s goal is: Gather resources, build your base, and then create an army that will strike fear into the heart of your enemy to defeat the Dark One, Amon.

The only condition to defeat Amon is to have a sufficient number of Zealots, which is a randomly generated integer between 20 and 100 by the system. Simultaneously, the system generates just enough unmined Crystal for you to defeat Amon based on this number.

Why just enough?

Since warping in Zealots requires a sufficient amount of Pylon energy and Crystal, and producing Pylons that provide energy also consumes Crystal. Therefore, if you build too many Pylons, although you might have enough energy, you will lose this battle that decides the fate of the entire universe because you don’t have enough Crystal to warp in Zealots.

The entire logic is provided by an object whose lifecycle spans the entire battle. To prevent issues, I added a thread lock to ensure each operation is atomic.

# Since it is the core of the entire game, I call it the Nexus
import random
from threading import Lock


class Nexus(object):
    _lock = Lock()
    crestalInControl = None
    crestalRemain = None
    populationCap = None
    zealot = None
    status = {}
    _amond = None

    def __init__(self):
        # Initialize object
        self._amond = random.randint(20, 100)
        self.crestalRemain = self._amond * 100 + (self._amond // 5 + 1) * 100
        self.crestalInControl = 0
        self.populationCap = 0
        self.zealot = 0

    def collect(self, amount=1000):
        # Mine Crystal
        with self._lock:
            amount = min(amount, self.crestalRemain)
            self.crestalRemain -= amount
            self.crestalInControl += amount
            return amount

    def transport(self, amount=5):
        # Warp in Zealots
        with self._lock:
            capacity = self.populationCap / 2
            available = self.crestalInControl / 100
            amount = min(amount, capacity, available)
            self.zealot += amount
            self.crestalInControl -= amount * 100
            self.populationCap -= amount * 2
            return amount

    def build(self, amount=1):
        # Build Pylon
        with self._lock:
            available = self.crestalInControl / 100
            amount = min(amount, available)
            self.populationCap += amount * 10
            self.crestalInControl -= amount * 100
            return amount

    def forAiur(self):
        # For Aiur!
        with self._lock:
            if self.zealot >= self._amond:
                return True
            else:
                return False

    def getStatus(self, role):
        # Get status
        if role == 'archon':
            return {
                'crestalInControl': self.crestalInControl,
                'crestalRemain': self.crestalRemain,
                'populationCap': self.populationCap,
                'zealot': self.zealot
            }
        elif role == 'pylon_transporter':
            return {
                'crestalInControl': self.crestalInControl,
                'populationCap': self.populationCap
            }
        elif role == 'portal':
            return {
                'crestalInControl': self.crestalInControl,
                'populationCap': self.populationCap,
                'zealot': self.zealot
            }
        else:
            return {}


nexus = Nexus() # Instantiate object

Conclusion

Actually, the game StarCraft II in each battle is a typical DAC model: the player dominates everything, and all operations in the game can be seen as transforming the two basic resources, Crystal and Vespene Gas (simplified away here), into the resources the player needs (production units, combat units) and consuming the enemy’s resources to win the battle. This shows that the RESTful service concept is very universal.

In the small game For Aiur, I solidified several roles to construct an RBAC model.

Through this practice, I gained an understanding of RBAC’s application in a system, further learned the excellent Flask framework, deepened my understanding of RESTful concepts, and gained a lot.

/System Design and Analysis/ /RESTful/ /RBAC/