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
Show file tree
Hide file tree
Changes from 17 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.
Empty file.
50 changes: 50 additions & 0 deletions kingdom/access/authentication/authenticator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
authenticator.py
Handles authentication throughout services
"""
from dataclasses import dataclass
from typing import Dict

import jwt

from src.core import config


class InvalidToken(Exception):
def __init__(self):
super().__init__("Invalid JWT token.")


@dataclass
class JWT:
"Simple JWT Wrapper"
token: str

def decode(self) -> Dict:
"""Decodes a JWT"""
try:
self.payload = jwt.decode(
jwt=self.token,
key=config.get_jwt_secret_key(),
algorithms=config.JWT_ALGORITHM,
)
return self.payload
except jwt.PyJWTError:
raise InvalidToken()


class Authenticator:
token: JWT
payload: Dict

def __init__(self, token: str):
self.jwt = JWT(token)

def __call__(self):
self.payload = self.jwt.decode()


def test_authenticator():
token = "abr9f8c09adj!lsd.sdxxf.ldfj"
auth = Authenticator(token)
assert auth()
82 changes: 82 additions & 0 deletions kingdom/access/authentication/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from datetime import datetime, timedelta
from typing import Dict, Generator, List, Optional, Tuple

import jwt

from kingdom.access.authorization.encode import encode
from kingdom.access.authorization.types import (
AccessRequest,
Conditional,
Permission,
Policy,
PolicyContext,
Resource,
dataclass,
)

RANDOM_KEY = "abcd00f"
JWT_ALGORITHM = "HS256"

TOKEN_EXPIRATION_MIN = 30


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

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


@dataclass
class User:
access_key: str
roles: List[Optional[Role]]

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


JWTDecodedPayload = Dict

MaybeBytes = Tuple[Optional[bytes], Optional[Exception]]
MaybePayload = Tuple[Optional[JWTDecodedPayload], Optional[Exception]]


def encode_user_policies(user: User) -> PolicyContext:
user_policies = list(user.resolve_policies())
return encode(user_policies)


def encode_user_payload(user: User) -> JWTDecodedPayload:
ruiconti marked this conversation as resolved.
Show resolved Hide resolved
expiration = datetime.utcnow() + timedelta(minutes=TOKEN_EXPIRATION_MIN)
return dict(
sub=user.access_key, exp=expiration, roles=encode_user_policies(user),
)


def encode_jwt(user: User) -> MaybeBytes:
payload = encode_user_payload(user)
try:
return (
jwt.encode(
payload=payload, key=RANDOM_KEY, algorithm=JWT_ALGORITHM
),
None,
)
except jwt.PyJWTError as exc:
return None, exc


def decode_jwt(token: bytes) -> MaybePayload:
try:
return (
jwt.decode(jwt=token, key=RANDOM_KEY, algorithms=[JWT_ALGORITHM]),
None,
)
except jwt.PyJWTError as exc:
return None, exc
Empty file.
201 changes: 201 additions & 0 deletions kingdom/access/authorization/dsl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# test_authorization.py
""""
dsl.py

SPECIFICATION of Conditionals:

Vocabulary. These contains current implementation expected limitations and
simplifications:
primary ::= "resource"
identifier ::= "id"
selector ::= "*" | string
attrref ::= primary "." identifier
or_expr ::= "||"
compr_op ::= "=="
cond ::= attrref compr_op selector
conds ::= (cond or_expr)*

There are two available selectors:
1. All instances: "*"
2. Individual instances: "<uuid>"

There are some conditions to selectors:
1. All selector must **always** be alone.

OBS.: This is in WIP and should be thoroughly simplified & documented.
"""

import string
from typing import List


def conditionals_split(sequence: str):
expressions = sequence.split("||")
if "" in expressions:
# meaning that we had a loose OR
return False
ruiconti marked this conversation as resolved.
Show resolved Hide resolved

parsed_expr = [expression.strip() for expression in expressions]
for expr in parsed_expr:
for char in expr:
if char.isspace():
# illegal whitespaces
return False

if char in set(string.punctuation):
# illegal characteres
return False
ruiconti marked this conversation as resolved.
Show resolved Hide resolved

return tuple(parsed_expr)


def parse_identifier(expression):
# since it's the beginning, we can strip this, to avoid overly
# complicated iterations :-)
expression = expression.strip()
identifier = ""
for idx, token in enumerate(expression):
if token.isidentifier():
identifier += token
else:
if token == "." and len(identifier) > 0:
return identifier, expression[idx:]
return (False,)


VALID_OPS = {"==", ">", "<", ">=", "<=", "!="}
VALID_OPS_TOKEN = {token for operator in VALID_OPS for token in operator}


def parse_reference(reference_expr):
reference = ""
parsing_idx = -1

# first we try to read all valid tokens after "." and before a valid
# operator
for idx, token in enumerate(reference_expr):
if idx == 0:
# first token **must** be a ".", no other one allowed
if token != ".":
return (False,)
parsing_idx = idx
continue

if token.isspace() or token in VALID_OPS_TOKEN:
# we have found our potential reference
parsing_idx = idx
break

if token.isidentifier():
reference += token
else:
return (False,)

# check for illegality on the rest of the expression
# meaning we must only accept white spaces between end of ref and operator
rest = reference_expr[parsing_idx:]
for idx, token in enumerate(rest):
if token in VALID_OPS_TOKEN:
return (reference, rest[idx:])

if token.isspace():
# the only acceptable token between a ref and operator
continue
else:
return (False,)


def parse_identifier_reference(expression):
identifier, *reference_expr = parse_identifier(expression)
if identifier is False:
return False
reference, *operator_expr = parse_reference(*reference_expr)
if reference is False:
return False

return (identifier.upper(), reference.upper())


def parse_operator(operator_expr):
operator_expr = operator_expr.strip() # just to make sure

operator = ""
parsing_idx = -1
# First we parse a known operator.
for idx, token in enumerate(operator_expr):
if token in VALID_OPS_TOKEN:
# the only valid thing that is being parsed
operator += token
else:
parsing_idx = idx
break

# Then we check if the rest is OK.
rest = operator_expr[parsing_idx:]
for idx, token in enumerate(rest):
if token == "'":
# valid stopping point
if operator in VALID_OPS:
return (operator, rest[idx:])
# invalid operator
return (False,)

if token.isspace():
continue
else:
# illegal characteres
return (False,)


def parse_selector(selector_expr):
ALL_TOKEN = "*"
selector_expr = selector_expr.strip()
selector = ""
parsing_idx = -1

def isselector(token):
return token.isnumeric() or token.isidentifier() or token == ALL_TOKEN

# First we try to find a selector.
for idx, token in enumerate(selector_expr):
if idx == 0:
# first token
if token != "'":
return (False,)
continue

if token == "'":
parsing_idx = idx + 1
break

if isselector(token):
selector += token
else:
return (False,)

# Edge case: are we dealig with an *?
if ALL_TOKEN in selector and selector != "*":
# plain comparison
return (False,)

rest = selector_expr[parsing_idx + 1:]
if len(rest) > 0 or len(selector) == 0:
# we shouldn't have anything left
return (False,)
return (selector,)
ruiconti marked this conversation as resolved.
Show resolved Hide resolved


def parse_expression(expr):
ruiconti marked this conversation as resolved.
Show resolved Hide resolved
identifier, *reference_expr = parse_identifier(expr)
if identifier is False:
return False
reference, *operator_expr = parse_reference(*reference_expr)
if reference is False:
return False
operator, *selector_expr = parse_operator(*operator_expr)
if operator is False:
return False
selector, *end = parse_selector(*selector_expr)
if selector is False:
return False
return (identifier, reference, operator, selector)
Loading