Skip to content

Commit

Permalink
feat: type-safe rules (#9)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ericmiguel authored Jan 26, 2024
1 parent 95a3d0c commit 5620ba8
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 24 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@


```python
@app.get("/", dependencies=[rules["finances:read"]])
@app.get("/", dependencies=[rules["finances"].READ])
def read_root():
return {"Hello": "World"}
```
Expand Down Expand Up @@ -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"}

Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@


```python
@app.get("/", dependencies=[rules["finances:read"]])
@app.get("/", dependencies=[rules["finances"].READ])
def read_root():
return {"Hello": "World"}
```
Expand Down Expand Up @@ -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"}

Expand Down
69 changes: 51 additions & 18 deletions missil/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -114,34 +114,73 @@ 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
lile the following:
```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
----------
Expand All @@ -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__ = [
Expand Down
4 changes: 2 additions & 2 deletions sample/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!"}
Expand Down

0 comments on commit 5620ba8

Please sign in to comment.