/flapi

Primary LanguagePythonMIT LicenseMIT

CircleCI Coverage Status CodeFactor Code style: black

Supported Versions

  • 3.6
  • 3.7

Installation

pip3 install --upgrade pip
pip3 install git+http://github.com/manoadamro/flapi

Docs

Planned Improvements


Flapi

TODO


JWT

jwt_handler = FlaskJwt("secret", lifespan=300)

def get_token():
    encoded_token = jwt_handler.generate_token({"uuid": "123"}, ["read:protected"])
    return {"jwt": token}

@protect(HasScopes("read:protected"))
def protected():
    return "success"

@protect(HasScopes("read:protected"), MatchValue("jwt:uuid", "url:uuid"))
def protected_user(uuid):
    return uuid

FlaskJwt(...)

Creates an instance of FlaskJwt

jwt_handler = FlaskJwt(secret, lifespan, **kwargs)

secret: str (required) String used to sign tokens.

lifespan: int (required) Lifespan of a token in seconds, after which it will be considered invalid

app: flask.Flask (default: None):Instance of a flask app (flask.Flask). If one is provided it will be initialised immediately

verify: bool (default: True) If false, token rules will not be verified.

auto_update: bool (default: False) Return a token with an updated expiry in every response to a request that contained a valid jwt.

algorithm: str (default: HS256) Algorithm used to sign tokens.

issuer: str, list, callable (default: None) Limit token usage by issuer or list of issuers. can be callable returning string or list of strings

audience: str, list, callable (default: None) Limit token usage by audience or list of audiences. can be callable returning string or list of strings

json_encoder: json.JSONEncoder (default: None) Json encoder used to serialise dicts

FlaskJwt.generate_token(...)

Generates a new token, stores the decoded version in global store (can be retrieved with FlaskJwt.current_token()) and returns the encoded version

jwt_handler = FlaskJwt(...)
jwt_handler.generate_token(fields, scopes, **kwargs)

fields: dict (required) Token body

scopes: list, callable (default: ()) List of string scopes eg. read:thing, write:thing. can be callable returning list of strings

algorithm: str (default: HS256) Algorithm used to sign tokens.

headers: dict (default: None) Token headers

not_before: float (default: None) UTC timestamp representing the earliest time that the token will be considered valid

lifespan: int (defalut: constructor definition) Lifespan of a token in seconds, after which it will be considered invalid. defaults to lifespan defined in constructor if not defined, otherwise overrides it.

FlaskJwt.current_token()

Returns the decoded token associated with the current request from global store

FlaskJwt.current_token()

protect(...)

@protect(*rules)
def some_method():
    ...

rules: one or more rules. see flapi.jwt.rules


JWT Rules

JwtRule(...)

Base class for all JWT rules.
Can be used to build custom rules

class CustomRule(JwtRule):
    """
    A simple example to show to to extend JwtRule
    Ensures a token contains all of the keys defined in constructor
    """
    def __init__(*expected_keys: Any):
        """
        constructor:
        defines a list of keys that must exist in a token
        """
        self.expected_keys: List[str] = expected_keys

    def __call__(self, token: Dict) -> bool:
        """
        call:
        must be implemented.
        takes a decoded token (dict).
        returns True or False
        """
        return all(i in token.items() for i in self.expected_keys())


rule = CustomRule("thing", "other_thing"):

rule({"thing" 123, "nope": 321})
False

rule({"thing" 123, "other_thing": 321})
True

If __call__ method is not implemented in a subclass of JwtRule, a NotImplementedException will be raised.

HasScopes(...)

Ensures that a token contains all of the defined scopes

rule = HasScopes("read:thing", "write:thing"):

rule({"some": "thing", "other": 123, "scp": ["read:thing", "write:other"]})
False

rule({"some": "thing", "other": 123, "scp": ["read:thing", "write:thing"]})
True

MatchValue(...)

Uses jsonpointer to ensure that two or more values are the same.
valid objects are:

  • header : flask.request.header
  • json : flask.request.json
  • url : flask.request.view_args
  • param : flask.request.args
  • form : flask.request.form
  • jwt : the active token
# url: /some/page/<id>
# id: "12345"

rule = MatchValue("jwt:id", "url:id")


rule({"id": "54321"})
False

rule({"id": "12345"})
True


rule = MatchValue("jwt:thing/id", "url:id")

rule({"thing": {"id": "12345"}})
True

rule({"id": "12345"})
False

Callback(...)

Takes one or more callables that must all return True in order for the check to be considered a pass.

def yes(token):
    return True

def no(token):
    return False


rule = Callback(no)

rule({"some": "thing"})
False


rule = Callback(yes)

rule({"some": "thing"})
True

AnyOf(...)

Considers the check passed if any of the defined rule pass

rule = AnyOf(
    HasScopes("read:thing", "write:thing"),
    MatchValue("jwt:id", "url:id"))

rule({"scp": ["read:thing", "write:other"]})
True

rule({"id": "54321"})
True

rule({"some": "thing"})
False

AllOf(...)

Considers the check passed if all of the defined rule pass

# url: /some/page/<id>
# id: "12345"

rule = AllOf(
    HasScopes("read:thing", "write:thing"),
    MatchValue("jwt:id", "url:id"))

rule({"scp": ["read:thing", "write:other"]})
False

rule({"id": "54321"})
False

rule({"id": "54321", "scp": ["read:thing", "write:other"]})
True

NoneOf(...)

Considers the check passed if none of the defined rule pass

# url: /some/page/<id>
# id: "12345"

rule = AllOf(
    HasScopes("read:thing", "write:thing"),
    MatchValue("jwt:id", "url:id"))

rule({"scp": ["read:thing", "write:other"]})
False

rule({"id": "54321"})
False

rule({"id": "54321", "scp": ["read:thing", "write:other"]})
False

rule({"some": "thing"})
True

Schema

def get_minimum_dob():
    return (datetime.datetime.utcnow() - datetime.timedelta(days=365.25 * 16)).date()


class Address(Schema):
    number = Int(min_value=0, nullable=False)
    post_code = Regex(
        re.compile("[a-zA-z]{2}[0-9] ?[0-9][a-zA-z]{2}"), nullable=False
    )


class Items(Schema):
    name = String(min_length=3, max_length=50, nullable=False)
    count = Int(min_value=0, default=0)


class Person(Schema):
    __strict__ = True
    name = String(min_length=3, max_length=50, nullable=False)
    address = Object(Address, nullable=False, strict=True)
    friends = Array(Uuid, default=[])
    items = Array(Object(Items, strict=False), default=[])
    date_of_birth = Date(max_value=get_minimum_dob, nullable=False)
    date_of_death = Date(max_value=datetime.date.today, nullable=True)

    @custom_property(int, float, nullable=False)
    def something(cls, value):
        return value * 2

Schema(...)

Base class for schema definitions

class MySchema(Schema):
    ...

Notes:

  • Anything (including methods) defined in schema classes will be considered a property. If you wish to hide an attribute/method you will need to prefix it with an underscore

  • You can define properties using a method without the @custom_property decorator. This will mean you are passed value with no checks having been done on it.

  • To mark a schema as "strict" (meaning extra keys are not accepted), add __strict__ = True as an attribute

protect(...)

@protect(MySchema)
def some_method():
    ...

schema: Schema, Property or Rule (required) The check that has to pass in order for the decorated method to be called. see flapi.schema.types


Schema Types

Property(...)

Base class for all JWT rules.
Can be used to build custom property

class MyProperty:
    def __init__(self, multiplier, **kwargs):
        # call super and tell Property we only accept ints or floats,
        # pass on any kwargs
        super(DateTime, self).__init__(int, float, **kwargs)
        self.multiplier = multiplier

    def __call__(self, item):
        # do the default checks by calling super(),
        # get back an updated value
        value = super(Array, self).__call__(value)

        # do the property thing
        return value * multiplier

types: tuple of types (required) Accepted value types

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

custom_property(...)

Does the same as Property but as a decorator!

@custom_property(int, float, nullable=False)
def something(cls, value):
    return value * 2

@custom_property(int, float, default=1)
def something_else(cls, value):
    return value * 3

types: tuple of types (required) Accepted value types

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Object(...)

Allows nesting of schemas

class Address(Schema):
    number = Int(min_value=0, nullable=False)
    post_code = Regex(
        re.compile("[a-zA-z]{2}[0-9] ?[0-9][a-zA-z]{2}"), nullable=False
    )

class Person(Schema):
    address = Object(Address, nullable=False, strict=True)

strict: bool (default False) overrides __strict__ attribute on schema definition

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Array(...)

Defines an array of items conforming to a Schema/Property definition

class MySchema(Schema):
    items = Array(Object(Item, strict=False), default=[])

min_length: Property or Rule (required)

min_length: int or callable returning int (default None) Minimum allowed array length

max_length: int or callable returning int (default None) Maximum allowed array length

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Notes:

  • Value will default to an empty array if none

Choice(...)

Ensures a value is equal to one from a defined set.

class MySchema(Schema):
    choice = Choice([Bool(), Int(), "1", "2"], nullable=False)

choices: list (required) A list containing specific valid values, Property definitions or a mix of the two.

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Notes:

  • If a value conforms to more than one choice, it will be validated against the first valid one.

Number(...)

Ensures a value is either an in or a float

class MySchema(Schema):
    number = Number(min_value=0, max_value=10, nullable=False, default=2)

min_value: int, float or callable returning int or float (default None) Minimum allowed value

max_value: int, float or callable returning int or float (default None) Maximum allowed value

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Int(...)

Ensures a value is an integer

class MySchema(Schema):
    number = Int(min_value=0, max_value=10, nullable=False, default=2)

min_value: int or callable returning int (default None) Minimum allowed value

max_value: int or callable returning int (default None) Maximum allowed value

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Float(...)

Ensures a value is a float

class MySchema(Schema):
    number = Float(min_value=0.0, max_value=6.5, nullable=False, default=2.5)

min_value: float or callable returning float (default None) Minimum allowed value

max_value: float or callable returning float (default None) Maximum allowed value

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Bool(...)

Ensures a value is either true or false

class MySchema(Schema):
    boolean = Bool(nullable=False, default=True)

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

String(...)

Ensures a value is a string

class MySchema(Schema):
    thing = String(min_length=2, max_length=5, nullable=False)

min_length: int or callable returning int (default None) Minimum allowed string length

max_length: int or callable returning int (default None) Maximum allowed string length

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Regex(...)

Ensures a value matches a regex string

class MySchema(Schema):
    thing = Regex(".+@[^@]+.[^@]{2,}$", min_length=2, max_length=5, nullable=False)

matcher: regex string or compiled pattern (required) Regex to match value against

min_length: int or callable returning int (default None) Minimum allowed string length

max_length: int or callable returning int (default None) Maximum allowed string length

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Email(...)

Ensures a value is a valid email address

class MySchema(Schema):
    thing = Email(nullable=False)

min_length: int or callable returning int (default None) Minimum allowed string length

max_length: int or callable returning int (default None) Maximum allowed string length

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Notes:

  • matcher used: .+@[^@]+.[^@]{2,}$

Uuid(...)

Ensures a value is a valid uuid

class MySchema(Schema):
    thing = Email(nullable=False, strip_hyphens=True)

strip_hyphens: bool (default False) If true, hyphens will be removed from the value string

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Notes:

  • matcher used: ^[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12}$

Date(...)

Ensures a value is either a valid iso8601 date or utc timestamp and parses to date object

class MySchema(Schema):
    date = Date(nullable=False, min_value=datetime.date.today)

min_value: date or callable returning date (default None) Minimum allowed date

max_value: date or callable returning date (default None) Maximum allowed date

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Notes:

  • format used: %Y-%m-%d

Datetime(...)

Ensures a value is a valid iso8601 datetime or utc timestamp and parses to datetime object

class MySchema(Schema):
    time = DateTime(nullable=False, min_value=datetime.datetime.now)

min_value: datetime or callable returning datetime (default None) Minimum allowed datetime

max_value: datetime or callable returning datetime (default None) Maximum allowed datetime

nullable: bool (default True) If false, an error will be raised if a null value is receeved

default: Any (default None) If a null value is a received, it will be replaced with this

callback: Callable (default None) A method to call once all checks are complete. This method receives the value as its only parameter and returns a modified value

Notes:

  • format used: %Y-%m-%dT%H:%M:%S.%f

  • accepts timezones in hh:mm format or Z

AllOf(...)

TODO description

# TODO example

TODO params

AnyOf(...)

TODO description

# TODO example

TODO params

NoneOf(...)

TODO description

# TODO example

TODO params


Callback(...)

TODO description

# TODO example

TODO params


Planned Improvements:

JWT

  • Allow MatchValue to use custom objects, probably accept tuple (callable, path) in place of string path.
  • Pass strings to jwt_protected and have them implicitly used as required scopes.
  • Pass a tuple to any collection rule and have it implicitly used as an AllOf
  • Allow more parameters to be resolved callables

Schema

  • Auto map to sqlalchemy model
  • Auto map to neomodel model
  • Define a key that is different to the attribute in schema class
  • Allow more parameters to be resolved callables