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

Added custom flask app wrapper to consume foca as config #200

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion foca/api/register_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def register_openapi(
# OpenAPI 3
sec_schemes = spec_parsed.get(
'components', {'securitySchemes': {}}
).get('securitySchemes', {}) # type: ignore
).get('securitySchemes', {})
for sec_scheme in sec_schemes.values():
sec_scheme[key] = val
logger.debug(f"Added security fields: {spec.add_security_fields}")
Expand Down
10 changes: 6 additions & 4 deletions foca/config/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from logging.config import dictConfig
from pathlib import Path
from typing import (Dict, Optional)
from typing import (Dict, Optional, Callable)

from addict import Dict as Addict
from pydantic import BaseModel
Expand Down Expand Up @@ -157,7 +157,10 @@ def parse_custom_config(self, model: str) -> BaseModel:
module = Path(model).stem
model_class = Path(model).suffix[1:]
try:
model_class = getattr(import_module(module), model_class)
model_class_instance: Callable = getattr(
import_module(module),
model_class
)
except ModuleNotFoundError:
raise ValueError(
f"failed validating custom configuration: module '{module}' "
Expand All @@ -169,8 +172,7 @@ def parse_custom_config(self, model: str) -> BaseModel:
f"has no class {model_class} or could not be imported"
)
try:
custom_config = model_class( # type: ignore[operator]
**self.config.custom) # type: ignore[attr-defined]
custom_config = model_class_instance(**self.config.custom)
except Exception as exc:
raise ValueError(
"failed validating custom configuration: provided custom "
Expand Down
3 changes: 2 additions & 1 deletion foca/errors/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ def _problem_handler_json(exception: Exception) -> Response:
JSON-formatted error response.
"""
# Look up exception & get status code
conf = current_app.config.foca.exceptions # type: ignore[attr-defined]
foca_conf = getattr(current_app.config, 'foca')
conf = foca_conf.exceptions
exc = type(exception)
if exc not in conf.mapping:
exc = Exception
Expand Down
8 changes: 4 additions & 4 deletions foca/factories/celery_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from inspect import stack
import logging

from flask import Flask
from connexion import FlaskApp
from celery import Celery

# Get logger instance
logger = logging.getLogger(__name__)


def create_celery_app(app: Flask) -> Celery:
def create_celery_app(app: FlaskApp) -> Celery:
"""Create and configure Celery application instance.

Args:
Expand All @@ -19,7 +19,7 @@ def create_celery_app(app: Flask) -> Celery:
Returns:
Celery application instance.
"""
conf = app.config.foca.jobs # type: ignore[attr-defined]
conf = app.config.foca.jobs

# Instantiate Celery app
celery = Celery(
Expand All @@ -32,7 +32,7 @@ def create_celery_app(app: Flask) -> Celery:
logger.debug(f"Celery app created from '{calling_module}'.")

# Update Celery app configuration with Flask app configuration
setattr(celery.conf, 'foca', app.config.foca) # type: ignore[attr-defined]
setattr(celery.conf, 'foca', app.config.foca)
logger.debug('Celery app configured.')

class ContextTask(celery.Task): # type: ignore
Expand Down
4 changes: 3 additions & 1 deletion foca/factories/connexion_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from connexion import App

from foca.models.config import Config
from foca.factories.flask_app import create_flask_app

# Get logger instance
logger = logging.getLogger(__name__)
Expand All @@ -26,6 +27,7 @@ def create_connexion_app(config: Optional[Config] = None) -> App:
__name__,
skip_error_handlers=True,
)
app.app = create_flask_app()

calling_module = ':'.join([stack()[1].filename, stack()[1].function])
logger.debug(f"Connexion app created from '{calling_module}'.")
Expand Down Expand Up @@ -71,7 +73,7 @@ def __add_config_to_connexion_app(
logger.debug('* {}: {}'.format(key, value))

# Add user configuration to Flask app config
setattr(app.app.config, 'foca', config)
app.app.config.foca = config

logger.debug('Connexion app configured.')
return app
35 changes: 35 additions & 0 deletions foca/factories/flask_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Factory for creating and configuring Connexion application instances."""

from flask import Config, Flask
from inspect import stack
import logging
from typing import Optional

from foca.models.config import Config as FocaConfig

# Get logger instance
logger = logging.getLogger(__name__)


class FocaFlaskAppConfig(Config):
"""Custom config class wrapper to include foca as an attribute
within config.
"""
foca: Optional[FocaConfig]


def create_flask_app() -> Flask:
"""Create and configure Flask application instance for connexion
context.

Returns:
Flask application with custom foca config configured.
"""

flask_app = Flask(__name__)
flask_app.config.from_object(FocaFlaskAppConfig)

calling_module = ':'.join([stack()[1].filename, stack()[1].function])
logger.debug(f"Flask app created from '{calling_module}'.")

return flask_app
7 changes: 5 additions & 2 deletions foca/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def _get_by_path(
Returns:
Value of innermost key.
"""
return reduce(operator.getitem, key_sequence, obj) # type: ignore
return reduce(operator.getitem, key_sequence, obj)


class ExceptionLoggingEnum(Enum):
Expand Down Expand Up @@ -1200,6 +1200,7 @@ class Config(FOCABaseConfig):
db: Database config parameters.
jobs: Background job config parameters.
log: Logger config parameters.
custom: Custom config parameters. (Added by consumers)

Attributes:
server: Server config parameters.
Expand All @@ -1209,6 +1210,7 @@ class Config(FOCABaseConfig):
db: Database config parameters.
jobs: Background job config parameters.
log: Logger config parameters.
custom: Custom config parameters. (Added by consumers)

Raises:
pydantic.ValidationError: The class was instantianted with an illegal
Expand Down Expand Up @@ -1248,7 +1250,7 @@ class 'werkzeug.exceptions.BadGateway'>: {'title': 'Bad Gateway', 'status': 50\
time}: {levelname:<8}] {message} [{name}]')}, handlers={'console': LogHandlerC\
onfig(class_handler='logging.StreamHandler', level=20, formatter='standard', s\
tream='ext://sys.stderr')}, root=LogRootConfig(level=10, handlers=['console'])\
))
custom=None))
"""
server: ServerConfig = ServerConfig()
exceptions: ExceptionConfig = ExceptionConfig()
Expand All @@ -1257,6 +1259,7 @@ class 'werkzeug.exceptions.BadGateway'>: {'title': 'Bad Gateway', 'status': 50\
db: Optional[MongoConfig] = None
jobs: Optional[JobsConfig] = None
log: LogConfig = LogConfig()
custom: Any = None

class Config:
"""Configuration for Pydantic model class."""
Expand Down
35 changes: 15 additions & 20 deletions foca/security/access_control/access_control_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from foca.utils.logging import log_traffic
from foca.errors.exceptions import BadRequest
from foca.models.config import AccessControlConfig

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -62,16 +63,13 @@ def putPermission(
"""
request_json = request.json
if isinstance(request_json, dict):
app_config = current_app.config
foca_conf = getattr(current_app.config, 'foca')
try:
security_conf = \
app_config.foca.security # type: ignore[attr-defined]
access_control_config = \
security_conf.access_control # type: ignore[attr-defined]
access_control_conf: AccessControlConfig = \
foca_conf.security.access_control
db_coll_permission: Collection = (
app_config.foca.db.dbs[ # type: ignore[attr-defined]
access_control_config.db_name]
.collections[access_control_config.collection_name].client
foca_conf.db.dbs[access_control_conf.db_name]
.collections[access_control_conf.collection_name].client
)

permission_data = request_json.get("rule", {})
Expand Down Expand Up @@ -102,11 +100,10 @@ def getAllPermissions(limit=None) -> List[Dict]:
Returns:
List of permission dicts.
"""
app_config = current_app.config
access_control_config = \
app_config.foca.security.access_control # type: ignore[attr-defined]
foca_conf = getattr(current_app.config, 'foca')
access_control_config = foca_conf.security.access_control
db_coll_permission: Collection = (
app_config.foca.db.dbs[ # type: ignore[attr-defined]
foca_conf.db.dbs[
access_control_config.db_name
].collections[access_control_config.collection_name].client
)
Expand Down Expand Up @@ -145,11 +142,10 @@ def getPermission(
Returns:
Permission data for the given id.
"""
app_config = current_app.config
access_control_config = \
app_config.foca.security.access_control # type: ignore[attr-defined]
foca_conf = getattr(current_app.config, 'foca')
access_control_config = foca_conf.security.access_control
db_coll_permission: Collection = (
app_config.foca.db.dbs[ # type: ignore[attr-defined]
foca_conf.db.dbs[
access_control_config.db_name
].collections[access_control_config.collection_name].client
)
Expand Down Expand Up @@ -181,11 +177,10 @@ def deletePermission(
Returns:
Delete permission identifier.
"""
app_config = current_app.config
access_control_config = \
app_config.foca.security.access_control # type: ignore[attr-defined]
foca_conf = getattr(current_app.config, 'foca')
access_control_config = foca_conf.security.access_control
db_coll_permission: Collection = (
app_config.foca.db.dbs[ # type: ignore[attr-defined]
foca_conf.db.dbs[
access_control_config.db_name
].collections[access_control_config.collection_name].client
)
Expand Down
6 changes: 3 additions & 3 deletions foca/security/access_control/foca_casbin_adapter/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from casbin import persist
from casbin.model import Model
from typing import (List, Optional)
from typing import (Any, Dict, List, Optional)
from pymongo import MongoClient

from foca.security.access_control.foca_casbin_adapter.casbin_rule import (
Expand Down Expand Up @@ -170,10 +170,10 @@ def remove_filtered_policy(
if not (1 <= field_index + len(field_values) <= 6):
return False

query = {}
query: Dict[str, Any] = {}
for index, value in enumerate(field_values):
query[f"v{index + field_index}"] = value

query["ptype"] = ptype # type: ignore[assignment]
query["ptype"] = ptype
results = self._collection.delete_many(query)
return results.deleted_count > 0
13 changes: 8 additions & 5 deletions foca/security/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@

from connexion.exceptions import Unauthorized
import logging
from typing import (Dict, Iterable, List, Optional)
from typing import (Any, cast, Dict, Iterable, List, Optional)

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from flask import current_app, request
from flask import (current_app, request)
import jwt
from jwt.exceptions import InvalidKeyError
import requests
from requests.exceptions import ConnectionError
import json
from werkzeug.datastructures import ImmutableMultiDict

from foca.models.config import Config

# Get logger instance
logger = logging.getLogger(__name__)

Expand All @@ -36,7 +38,8 @@ def validate_token(token: str) -> Dict:
oidc_config_claim_public_keys: str = 'jwks_uri'

# Fetch security parameters
conf = current_app.config.foca.security.auth # type: ignore[attr-defined]
foca_conf = cast(Config, getattr(current_app.config, 'foca'))
conf = foca_conf.security.auth
add_key_to_claims: bool = conf.add_key_to_claims
allow_expired: bool = conf.allow_expired
audience: Optional[Iterable[str]] = conf.audience
Expand Down Expand Up @@ -245,10 +248,10 @@ def _validate_jwt_public_key(
validation_options['verify_exp'] = False

# Try public keys one after the other
used_key: Dict = {}
used_key: Any = {}
claims = {}
for key in public_keys.values():
used_key = key # type: ignore[assignment]
used_key = key

# Decode JWT and validate via public key
try:
Expand Down
13 changes: 9 additions & 4 deletions tests/errors/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
_subset_nested_dict,
)
from foca.models.config import Config
from foca.factories.flask_app import FocaFlaskAppConfig

EXCEPTION_INSTANCE = Exception()
INVALID_LOG_FORMAT = 'unknown_log_format'
Expand Down Expand Up @@ -103,7 +104,8 @@ def test__exclude_key_nested_dict():
def test__problem_handler_json():
"""Test problem handler with instance of custom, unlisted error."""
app = Flask(__name__)
setattr(app.config, 'foca', Config())
app.config.from_object(FocaFlaskAppConfig)
app.config.foca = Config()
EXPECTED_RESPONSE = app.config.foca.exceptions.mapping[Exception]
with app.app_context():
res = _problem_handler_json(UnknownException())
Expand All @@ -117,7 +119,8 @@ def test__problem_handler_json():
def test__problem_handler_json_no_fallback_exception():
"""Test problem handler; unlisted error without fallback."""
app = Flask(__name__)
setattr(app.config, 'foca', Config())
app.config.from_object(FocaFlaskAppConfig)
app.config.foca = Config()
del app.config.foca.exceptions.mapping[Exception]
with app.app_context():
res = _problem_handler_json(UnknownException())
Expand All @@ -131,7 +134,8 @@ def test__problem_handler_json_no_fallback_exception():
def test__problem_handler_json_with_public_members():
"""Test problem handler with public members."""
app = Flask(__name__)
setattr(app.config, 'foca', Config())
app.config.from_object(FocaFlaskAppConfig)
app.config.foca = Config()
app.config.foca.exceptions.public_members = PUBLIC_MEMBERS
with app.app_context():
res = _problem_handler_json(UnknownException())
Expand All @@ -143,7 +147,8 @@ def test__problem_handler_json_with_public_members():
def test__problem_handler_json_with_private_members():
"""Test problem handler with private members."""
app = Flask(__name__)
setattr(app.config, 'foca', Config())
app.config.from_object(FocaFlaskAppConfig)
app.config.foca = Config()
app.config.foca.exceptions.private_members = PRIVATE_MEMBERS
with app.app_context():
res = _problem_handler_json(UnknownException())
Expand Down
13 changes: 13 additions & 0 deletions tests/factories/test_flask_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Tests for foca.factories.flask_app."""
from flask import Flask

from foca.factories.flask_app import create_flask_app
from foca.models.config import Config


def test_create_flask_app():
"""Test Connexion app creation without config."""
flask_app = create_flask_app()
assert isinstance(flask_app, Flask)
flask_app.config.foca = Config()
assert isinstance(flask_app.config.foca, Config)
Loading