From 5620ba80b0225dc9ccaa581653471e62780cd4bf Mon Sep 17 00:00:00 2001 From: Eric Miguel Date: Fri, 26 Jan 2024 15:24:03 -0300 Subject: [PATCH] feat: type-safe rules (#9) * feat: new Area class with WRITE and READ attributes. Area READ and WRITE attributes are Rule class objects already initialized using 0 and 1 literals. * docs: readme and mkdocs properly updated to reflect API changes --- README.md | 4 +-- docs/index.md | 4 +-- missil/__init__.py | 69 ++++++++++++++++++++++++++++++++++------------ sample/main.py | 4 +-- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b99dbfa..46526da 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ```python -@app.get("/", dependencies=[rules["finances:read"]]) +@app.get("/", dependencies=[rules["finances"].READ]) def read_root(): return {"Hello": "World"} ``` @@ -58,7 +58,7 @@ SECRET_KEY = "2ef9451be5d149ceaf5be306b5aa03b41a0331218926e12329c5eeba60ed5cf0" bearer = missil.FlexibleTokenBearer(TOKEN_KEY, SECRET_KEY) rules = missil.make_rules(bearer, "finances", "it", "other") -@app.get("/", dependencies=[rules["finances:read"]]) +@app.get("/", dependencies=[rules["finances"].READ]) def read_root(): return {"Hello": "World"} diff --git a/docs/index.md b/docs/index.md index ea54911..8cd6c90 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ ```python -@app.get("/", dependencies=[rules["finances:read"]]) +@app.get("/", dependencies=[rules["finances"].READ]) def read_root(): return {"Hello": "World"} ``` @@ -64,7 +64,7 @@ SECRET_KEY = "2ef9451be5d149ceaf5be306b5aa03b41a0331218926e12329c5eeba60ed5cf0" bearer = missil.FlexibleTokenBearer(TOKEN_KEY, SECRET_KEY) rules = missil.make_rules(bearer, "finances", "it", "other") -@app.get("/", dependencies=[rules["finances:read"]]) +@app.get("/", dependencies=[rules["finances"].READ]) def read_root(): return {"Hello": "World"} diff --git a/missil/__init__.py b/missil/__init__.py index 258d799..0f0db3d 100644 --- a/missil/__init__.py +++ b/missil/__init__.py @@ -21,7 +21,7 @@ READ = 0 WRITE = 1 -DENY = -1 +DENY = -1 # TODO: will overlap READ and WRITE permissions, but not implemented yet class Rule(FastAPIDependsClass): @@ -44,7 +44,7 @@ def __init__( Parameters ---------- area : str - Business area name like 'financial' or 'human resources'. + Business area name, like 'financial' or 'human resources'. level : int Access level: READ = 0 / WRITE = 1. bearer : TokenBearer @@ -114,11 +114,50 @@ def dependency(self, _: Any) -> None: pass -def make_rules(bearer: TokenBearer, *areas: str) -> dict[str, Rule]: +class Area: + """ + Business area. + + A business area instance holds up READ and WRITE rules as attributes, which + can be injected as a FastAPI endpoint dependency. For example: + + ```python + bearer = ... + finances = Area('finances', bearer) + + @app.get("/finances/read", dependencies=[finances.READ]) + def finances_read() -> dict[str, str]: + ... + ``` + """ + + def __init__(self, name: str, bearer: TokenBearer) -> None: + """ + Creates a business area object. + + Parameters + ---------- + name : str + Business area name. + bearer : TokenBearer + JWT token source source. See Bearers module. + """ + self.name: str = name + self.bearer = bearer + self.READ = Rule(self.name, 0, self.bearer) + self.WRITE = Rule(self.name, 1, self.bearer) + + +def make_rules(bearer: TokenBearer, *areas: str) -> dict[str, Area]: """ Create a Missil ruleset, conveniently. - A ruleset is a simple dict bearing endpoint-appliable rules (dict[str, Rule]. + A ruleset is a simple mapping bearing endpoint-appliable rule + bearers + + ```python + rules: dict[str, Area] = make_rules(..., ...). + ``` Given a token source (see Bearers module) and some business area names, like "it", "finances", "hr", this function will return something @@ -126,22 +165,22 @@ def make_rules(bearer: TokenBearer, *areas: str) -> dict[str, Rule]: ```python { - 'it:read': Rule, - 'it:write': Rule, - 'finances:read': Rule + 'it': Area, + 'finances': Area, + 'hr': Area ... } ``` - So, one can pass like a FastAPI dependency like: + So, one can pass like a FastAPI dependency, as shown in the following example: ```python - @app.get("/items/{item_id}", dependencies=[rules["finances:read"]]) + @app.get("/items/{item_id}", dependencies=[rules["finances"].READ]) def read_item(item_id: int, q: Union[str, None] = None): ... ``` - See the sample API (sample/main.py) to a working usage example. + See the sample API (sample/main.py) to a folly working usage example. Parameters ---------- @@ -150,16 +189,10 @@ def read_item(item_id: int, q: Union[str, None] = None): Returns ------- - dict[str, Rule] + dict[str, Area] Dict containing endpoint-appliable rules. """ - rules = {} - for level in (READ, WRITE): - for area in areas: - level_name = "read" if level <= 0 else "write" - rules.update({f"{area}:{level_name}": Rule(area, level, bearer)}) - - return rules + return {area: Area(area, bearer) for area in areas} __all__ = [ diff --git a/sample/main.py b/sample/main.py index 122396c..5773b27 100644 --- a/sample/main.py +++ b/sample/main.py @@ -54,13 +54,13 @@ def set_cookies(response: Response) -> dict[str, str]: return {"msg": "The Authorization token is stored as a cookie."} -@app.get("/finances/read", dependencies=[rules["finances:read"]]) +@app.get("/finances/read", dependencies=[rules["finances"].READ]) def finances_read() -> dict[str, str]: """Requires read permission on finances.""" return {"msg": "you have permission to perform read actions on finances!"} -@app.get("/finances/write", dependencies=[rules["finances:write"]]) +@app.get("/finances/write", dependencies=[rules["finances"].WRITE]) def finances_write() -> dict[str, str]: """Requires write permission on finances.""" return {"msg": "you have permission to perform write actions on finances!"}