Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authorization core module #19

Merged
merged 32 commits into from
Apr 23, 2021
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0813d32
Feat: First draft of attribute conditional DSL
ruiconti Apr 2, 2021
a2de890
Improve: Finished parsing a policy statement
ruiconti Apr 5, 2021
89b9c2c
Feat: Permission check design
ruiconti Apr 7, 2021
6e3c655
Feat: Implements policy simple and redundant packing
ruiconti Apr 8, 2021
5dd9a0a
Feat: Finishes whole checking verification
ruiconti Apr 9, 2021
f3804ce
Chore: Proper structure changing and documenting
ruiconti Apr 13, 2021
d3f0b97
Chore: Splits tests from implementation
ruiconti Apr 13, 2021
b131071
Chore: Move tests around
ruiconti Apr 13, 2021
9108880
Refactor: Encoding a PolicyContext now uses Permission.value for easi…
ruiconti Apr 13, 2021
8ab4d30
Chore: Renames models to types
ruiconti Apr 13, 2021
3cb2917
Chore: Updates imports
ruiconti Apr 13, 2021
1da28e5
Feat: Initial steps of final module interface
ruiconti Apr 13, 2021
2d93ea6
Feat: Implements identity flow
ruiconti Apr 14, 2021
1c19012
improve: More knowledge to AccessRequest
ruiconti Apr 14, 2021
0ce5419
chore: Import here to avoid cyclic dependencies
ruiconti Apr 14, 2021
4228f2c
improve: Checking write permission is agnostic to input types
ruiconti Apr 14, 2021
f927c7e
chore: Modifies some helpers
ruiconti Apr 14, 2021
1e60aa2
Feat: Redesign and structuring
ruiconti Apr 16, 2021
8101129
Chore: Improve and fix a few docstrings and namings
ruiconti Apr 16, 2021
3e51b8b
Chore: Organizes and improves types cohesion
ruiconti Apr 16, 2021
31bcd18
Chore: Further improves docstrings on permission checking
ruiconti Apr 16, 2021
b53153a
Chore: Removes dead code
ruiconti Apr 16, 2021
f6fc132
Chore: Centers TOKEN_ALL in its proper DSL module
ruiconti Apr 16, 2021
cdb2007
Chore: Simplifies context mapping
ruiconti Apr 20, 2021
494922a
Feat: Adds a test case for unmapped Permission Enum
ruiconti Apr 20, 2021
af79e5d
Refactor: Permission union now uses raw integers
ruiconti Apr 20, 2021
8ffb999
Fix: Semantic typo
ruiconti Apr 20, 2021
f3b1ea5
Refactor: Inline statement using OR
ruiconti Apr 20, 2021
28718e3
Refactor: Greatly simplifies write checking conditions
ruiconti Apr 20, 2021
0ffe1c0
Refactor: Tests whether an empty scope raises UnauthorizedException
ruiconti Apr 20, 2021
5b60a12
Refactor: An empty selector now raises UnauthorizedException
ruiconti Apr 20, 2021
4c979ff
Fix: Adds a missing corner case for conditional splitting
ruiconti Apr 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion echo

This file was deleted.

Empty file added kingdom/__init__.py
Empty file.
Empty file added kingdom/access/__init__.py
Empty file.
312 changes: 312 additions & 0 deletions kingdom/access/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from functools import reduce
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union

from kingdom.access import config
from kingdom.access.dsl import TOKEN_ALL
from kingdom.access.types import Payload, PolicyContext, SelectorPermissionMap


class Permission(Enum):
"""Fine-grained mapping of a permission operation."""

READ = 0b000
CREATE = 0b001
UPDATE = 0b010
DELETE = 0b100

def __hash__(self) -> int:
return hash(self.value)

def __or__(self, other):
if isinstance(other, Permission):
other = other.value
return self.value | other

def __ror__(self, other):
return self.__or__(other)

def __and__(self, other):
if isinstance(other, Permission):
other = other.value
return self.value & other

def __rand__(self, other):
return self.__and__(other)


@dataclass
class Resource:
"""An enterprise-wide resource wrapper"""

name: str

def __init__(self, name: str):
self.name = name
self.alias = name.lower()


@dataclass
class Statement:
"""One and only one conditional statement"""

identifier: str
selector: str


@dataclass
class Policy:
"""
An association of permissions that are allowed to be performed on
on instances of a given resource that match ANY of the conditionals
criteria"""

resource: Resource
permissions: Tuple[Permission, ...]
conditionals: List[Statement]


@dataclass
class Role:
name: str
policies: List[Policy]

def __hash__(self) -> int:
return hash(self.name)


@dataclass
class User:
access_key: str
roles: List[Optional[Role]]
ruiconti marked this conversation as resolved.
Show resolved Hide resolved

@property
def policy_context(self) -> PolicyContext:
"Builds a policy context reading all roles associated to a User"

user_policies = list(self.resolve_policies())
return encode_policies(user_policies)

@property
def jwt_payload(self) -> Payload:
"Knows how to build a JWT Payload with necessary claims"

expiration = datetime.utcnow() + timedelta(
minutes=config.TOKEN_EXPIRATION_MIN
)
return dict(
sub=self.access_key, exp=expiration, policies=self.encoded_policies
)

def resolve_policies(self):
# TODO: Improve this typing
for role in self.roles:
for policy in role.policies:
yield policy


PermissionTuple = Tuple[Permission, ...]
AnyPermission = Union[Permission, int]
PermissionTupleOrInt = Union[PermissionTuple, int]


def ptoi(permissions: PermissionTupleOrInt) -> int:
"""
Considering that a permission can be both an integer and an instance of
Permission, this function ensures that operations are done with a
Permission type.
It does by translating integer to Permission whenever needed.

>>> ptoi(0)
0
>>> ptoi((Permission.CREATE, Permission.READ, Permission.UPDATE))
3
>>> ptoi((,))
0
"""
if isinstance(permissions, int):
return permissions

return (
reduce(lambda a, b: a | b, permissions)
if len(permissions) > 1
else permissions[0].value
if len(permissions) == 1
else 0
)


def union(
permissions: PermissionTupleOrInt,
incoming_permissions: PermissionTupleOrInt,
) -> int:
"""
Given a set of permissions and a set of incoming permissions that should
be unionized, this method adds them up.

>>> union_permissions((Permission.READ,), 2)
3
>>> union_permissions(
(Permission.DELETE,), (Permission.CREATE, Permission.UPDATE))
7
>>> union_permissions((Permission.CREATE,), (Permission.CREATE,))
1
"""
return ptoi(permissions) | ptoi(incoming_permissions)


def build_redundant_context(policies: List[Policy]) -> PolicyContext:
ruiconti marked this conversation as resolved.
Show resolved Hide resolved
"""
Parses a list of Policy and return a mapping of
resource per selector per permission, known as PolicyContext.

Warning: This map is redundant i.e. "*" might have overlapping permissions
with some other specific selectors. Take this example:

>>> a_policy = Policy(
resource=Resource("Account"),
permissions=(Permission.UPDATE, Permission.CREATE,),
conditionals=[Statement("resource.id", "*")]
)
>>> ya_policy = Policy(
resource=Resource("Account"),
permissions=(Permission.UPDATE,),
conditionals=[Statement("resource.id", "5f34")]
)
>>> build_redundant_context([a_policy, ya_policy])
{
"account": {
"*": 3,
"5f34": 2,
}
}

Note that "5f34" entry is redundant because "*" selector already
contemplates this statement condition.
"""

def pivot_policy(
current_mapping: SelectorPermissionMap, policy: Policy
) -> SelectorPermissionMap:
"""Pivots a Policy into a map of Selector: PermissionInt
It updates Permission value with context's permissions"""
return {
conditional.selector: union(
permissions,
current_mapping.get(conditional.selector, tuple()),
)
for conditional in policy.conditionals
}

context: PolicyContext = {}

for policy in policies:
# Aliasing:
resource = policy.resource.alias
permissions: PermissionTuple = policy.permissions
if resource not in context:
context[resource] = defaultdict(dict)

context[resource].update(pivot_policy(context[resource], policy))

return context


def remove_context_redundancy(context: PolicyContext) -> PolicyContext:
"""
Given a redundant PolicyContext, remove any redundancy that might exist
For now, redundancies origins from having to deal with "*" token.

>>> a_policy = Policy(
resource=Resource("Account"),
permissions=(Permission.UPDATE, Permission.CREATE,),
conditionals=[Statement("resource.id", "*")]
)
>>> ya_policy = Policy(
resource=Resource("Account"),
permissions=(Permission.UPDATE),
conditionals=[Statement("resource.id", "5f34")]
)
>>> ctx = build_redundant_context([a_policy, ya_policy])
>>> ctx
{
"account": {
"*": 3,
"5f34": 2,
}
}
>>> remove_context_redundancy(ctx)
{
"account": {
"*": 3,
}
}
"""
simplified: PolicyContext = deepcopy(context)

for resource, selector_perm in context.items():
if TOKEN_ALL not in selector_perm:
# Well, nothing to do then.
continue

for selector, permissions in selector_perm.items():
# Subtract "*"'s permissions from the other permissions
if selector == TOKEN_ALL:
# Not this one.
continue

updated_perms: int = permissions & ~selector_perm[TOKEN_ALL]
if updated_perms == 0:
# All of this selector's permissions are already
# contemplated by "*"
del simplified[resource][selector]
else:
simplified[resource][selector] = updated_perms

return simplified


def encode_policies(policies: List[Policy]) -> PolicyContext:
"""
This functions parses a list of Policy and return a mapping of
resource-selector-perimssion, known as PolicyContext.
This mapping should also contain a simplification of redundant policies
that a subject might have.

>>> a_policy = Policy(
resource=Resource("Account"),
permissions=(Permission.UPDATE,),
conditionals=[Statement("resource.id", "*")]
)
>>> ya_policy = Policy(
resource=Resource("Product"),
permissions=(Permission.READ,),
conditionals=[Statement("resource.id", "5f34")]
)
>>> pack_policies([a_policy, ya_policy])
{
"product": {
"5f34": Permission.READ.value,
},
"account": {
"*": Permission.UPDATE.value,
},
}

# Which is the equivalent of:
>>> pack_policies([a_policy, ya_policy])
{
"product": {
"5f34": 0,
},
"account": {
"*": 4,
},
}
"""
context = build_redundant_context(policies)
return remove_context_redundancy(context)
4 changes: 4 additions & 0 deletions kingdom/access/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
TOKEN_ALL = "*"
RANDOM_KEY = "abcd00f"
JWT_ALGORITHM = "HS256"
TOKEN_EXPIRATION_MIN = 30
Loading