Island: Reduce coupling between resources and flask

This commit is contained in:
vakarisz 2022-05-25 11:19:24 +03:00
parent 87082f058d
commit 84c78c4d8f
41 changed files with 86 additions and 143 deletions

View File

@ -11,6 +11,7 @@ from werkzeug.exceptions import NotFound
from common import DIContainer
from monkey_island.cc.database import database, mongo
from monkey_island.cc.resources import RemoteRun
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.agent_controls import StopAgentCheck, StopAllAgents
from monkey_island.cc.resources.attack.attack_report import AttackReport
from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt
@ -26,7 +27,6 @@ from monkey_island.cc.resources.configuration_import import ConfigurationImport
from monkey_island.cc.resources.edge import Edge
from monkey_island.cc.resources.exploitations.manual_exploitation import ManualExploitation
from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.island_configuration import IslandConfiguration
from monkey_island.cc.resources.island_logs import IslandLog
from monkey_island.cc.resources.island_mode import IslandMode
@ -121,7 +121,9 @@ class FlaskDIWrapper:
self._container = container
self._reserved_urls = set()
def add_resource(self, resource: Type[IResource]):
def add_resource(self, resource: Type[AbstractResource]):
assert "urls" in resource.__dict__, f"Resource {resource} has no defined URLs"
self._reserve_urls(resource.urls)
dependencies = self._container.resolve_dependencies(resource)

View File

@ -0,0 +1,6 @@
import flask_restful
# The purpose of this class is to decouple resources from flask
class AbstractResource(flask_restful.Resource):
pass

View File

@ -1,10 +1,8 @@
import flask_restful
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.services.infection_lifecycle import should_agent_die
class StopAgentCheck(flask_restful.Resource, IResource):
class StopAgentCheck(AbstractResource):
urls = ["/api/monkey-control/needs-to-stop/<int:monkey_guid>"]
def get(self, monkey_guid: int):

View File

@ -1,15 +1,14 @@
import json
import flask_restful
from flask import make_response, request
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.utils.semaphores import agent_killing_mutex
from monkey_island.cc.services.infection_lifecycle import set_stop_all, should_agent_die
class StopAllAgents(flask_restful.Resource, IResource):
class StopAllAgents(AbstractResource):
urls = ["/api/monkey-control/stop-all-agents"]
@jwt_required

View File

@ -1,13 +1,12 @@
import flask_restful
from flask import current_app, json
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.attack.attack_report import AttackReportService
from monkey_island.cc.services.attack.attack_schema import SCHEMA
class AttackReport(flask_restful.Resource, IResource):
class AttackReport(AbstractResource):
urls = ["/api/report/attack"]
@jwt_required

View File

@ -1,15 +1,11 @@
import logging
from functools import wraps
import flask_jwt_extended
import flask_restful
from flask import make_response, request
from flask_jwt_extended.exceptions import JWTExtendedException
from jwt import PyJWTError
from common.utils.exceptions import IncorrectCredentialsError
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services import AuthenticationService
logger = logging.getLogger(__name__)
@ -22,7 +18,7 @@ def init_jwt(app):
)
class Authenticate(flask_restful.Resource, IResource):
class Authenticate(AbstractResource):
"""
Resource for user authentication. The user provides the username and password and we
give them a JWT.

View File

@ -1,17 +1,16 @@
import logging
import flask_restful
from flask import make_response, request
from common.utils.exceptions import AlreadyRegisteredError, InvalidRegistrationCredentialsError
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.credential_utils import get_username_password_from_request
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services import AuthenticationService
logger = logging.getLogger(__name__)
class Registration(flask_restful.Resource, IResource):
class Registration(AbstractResource):
urls = ["/api/registration"]

View File

@ -2,8 +2,8 @@ import logging
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.attack.attack_report import AttackReportService
from monkey_island.cc.services.reporting.report import ReportService
@ -12,7 +12,7 @@ NOT_ALL_REPORTS_DELETED = "Not all reports have been cleared from the DB!"
logger = logging.getLogger(__name__)
class ClearCaches(flask_restful.Resource, IResource):
class ClearCaches(AbstractResource):
urls = ["/api/test/clear-caches"]
"""
Used for timing tests - we want to get actual execution time of functions in BlackBox without

View File

@ -1,13 +1,12 @@
import flask_restful
from bson import json_util
from flask import request
from monkey_island.cc.database import database, mongo
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
class LogBlackboxEndpoint(flask_restful.Resource, IResource):
class LogBlackboxEndpoint(AbstractResource):
urls = ["/api/test/log"]
@jwt_required

View File

@ -1,13 +1,12 @@
import flask_restful
from bson import json_util
from flask import request
from monkey_island.cc.database import mongo
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
class MonkeyBlackboxEndpoint(flask_restful.Resource, IResource):
class MonkeyBlackboxEndpoint(AbstractResource):
urls = ["/api/test/monkey"]
@jwt_required

View File

@ -1,13 +1,12 @@
import flask_restful
from bson import json_util
from flask import request
from monkey_island.cc.models.telemetries import get_telemetry_by_query
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
class TelemetryBlackboxEndpoint(flask_restful.Resource, IResource):
class TelemetryBlackboxEndpoint(AbstractResource):
urls = ["/api/test/telemetry"]
@jwt_required

View File

@ -1,15 +1,14 @@
import json
import flask_restful
from flask import request
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.server_utils.encryption import PasswordBasedStringEncryptor
from monkey_island.cc.services.config import ConfigService
class ConfigurationExport(flask_restful.Resource, IResource):
class ConfigurationExport(AbstractResource):
urls = ["/api/configuration/export"]
@jwt_required

View File

@ -3,12 +3,11 @@ import logging
from dataclasses import dataclass
from json.decoder import JSONDecodeError
import flask_restful
from flask import request
from common.utils.exceptions import InvalidConfigurationError
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.server_utils.encryption import (
InvalidCiphertextError,
InvalidCredentialsError,
@ -39,7 +38,7 @@ class ResponseContents:
return self.__dict__
class ConfigurationImport(flask_restful.Resource, IResource):
class ConfigurationImport(AbstractResource):
urls = ["/api/configuration/import"]
SUCCESS = False

View File

@ -1,11 +1,10 @@
import flask_restful
from flask import request
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService
class Edge(flask_restful.Resource, IResource):
class Edge(AbstractResource):
urls = ["/api/netmap/edge"]
def get(self):

View File

@ -1,13 +1,11 @@
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.reporting.exploitations.manual_exploitation import (
get_manual_exploitations,
)
class ManualExploitation(flask_restful.Resource, IResource):
class ManualExploitation(AbstractResource):
urls = ["/api/exploitations/manual"]
@jwt_required

View File

@ -1,13 +1,11 @@
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import (
get_monkey_exploited,
)
class MonkeyExploitation(flask_restful.Resource, IResource):
class MonkeyExploitation(AbstractResource):
urls = ["/api/exploitations/monkey"]
@jwt_required

View File

@ -1,17 +0,0 @@
from abc import ABCMeta, abstractmethod
from typing import Sequence
from flask.views import MethodViewType
# Flask resources inherit from flask_restful.Resource, so custom interface
# must implement both metaclasses
class AbstractResource(ABCMeta, MethodViewType):
pass
class IResource(metaclass=AbstractResource):
@property
@abstractmethod
def urls(self) -> Sequence[str]:
pass

View File

@ -1,14 +1,13 @@
import json
import flask_restful
from flask import abort, jsonify, request
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.config import ConfigService
class IslandConfiguration(flask_restful.Resource, IResource):
class IslandConfiguration(AbstractResource):
urls = ["/api/configuration/island"]

View File

@ -1,15 +1,13 @@
import logging
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.island_logs import IslandLogService
logger = logging.getLogger(__name__)
class IslandLog(flask_restful.Resource, IResource):
class IslandLog(AbstractResource):
urls = ["/api/log/island/download"]
@jwt_required

View File

@ -1,11 +1,10 @@
import json
import logging
import flask_restful
from flask import make_response, request
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.config_manipulator import update_config_on_mode_set
from monkey_island.cc.services.mode.island_mode_service import ModeNotSetError, get_mode, set_mode
from monkey_island.cc.services.mode.mode_enum import IslandModeEnum
@ -13,7 +12,7 @@ from monkey_island.cc.services.mode.mode_enum import IslandModeEnum
logger = logging.getLogger(__name__)
class IslandMode(flask_restful.Resource, IResource):
class IslandMode(AbstractResource):
urls = ["/api/island-mode"]
@jwt_required

View File

@ -1,16 +1,15 @@
import json
import flask_restful
from flask import jsonify, make_response, request
from monkey_island.cc.models import Monkey
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService
class LocalRun(flask_restful.Resource, IResource):
class LocalRun(AbstractResource):
urls = ["/api/local-monkey"]

View File

@ -1,18 +1,17 @@
import json
import flask_restful
from bson import ObjectId
from flask import request
from monkey_island.cc.database import mongo
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.log import LogService
from monkey_island.cc.services.node import NodeService
class Log(flask_restful.Resource, IResource):
class Log(AbstractResource):
urls = ["/api/log"]
@jwt_required

View File

@ -1,13 +1,12 @@
import json
from datetime import datetime
import flask_restful
from flask import request
from monkey_island.cc.database import mongo
from monkey_island.cc.models.monkey_ttl import create_monkey_ttl_document
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.utils.semaphores import agent_killing_mutex
from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
from monkey_island.cc.services.config import ConfigService
@ -17,7 +16,7 @@ from monkey_island.cc.services.node import NodeService
# TODO: separate logic from interface
class Monkey(flask_restful.Resource, IResource):
class Monkey(AbstractResource):
urls = [
"/api/agent",
"/api/agent/<string:guid>",

View File

@ -2,10 +2,9 @@ import hashlib
import logging
from pathlib import Path
import flask_restful
from flask import make_response, send_from_directory
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH
logger = logging.getLogger(__name__)
@ -20,7 +19,7 @@ class UnsupportedOSError(Exception):
pass
class MonkeyDownload(flask_restful.Resource, IResource):
class MonkeyDownload(AbstractResource):
urls = ["/api/agent/download/<string:host_os>"]
# Used by monkey. can't secure.

View File

@ -1,12 +1,10 @@
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.netmap.net_edge import NetEdgeService
from monkey_island.cc.services.netmap.net_node import NetNodeService
class NetMap(flask_restful.Resource, IResource):
class NetMap(AbstractResource):
urls = ["/api/netmap"]
@jwt_required

View File

@ -1,12 +1,11 @@
import flask_restful
from flask import request
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.node import NodeService
class Node(flask_restful.Resource, IResource):
class Node(AbstractResource):
urls = ["/api/netmap/node"]
@jwt_required

View File

@ -1,11 +1,9 @@
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.utils.node_states import NodeStates as NodeStateList
class NodeStates(flask_restful.Resource, IResource):
class NodeStates(AbstractResource):
urls = ["/api/netmap/node-states"]
@jwt_required

View File

@ -1,15 +1,14 @@
import logging
import flask_restful
from flask import make_response, send_file
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.services import FileRetrievalError, IFileStorageService
logger = logging.getLogger(__file__)
class PBAFileDownload(flask_restful.Resource, IResource):
class PBAFileDownload(AbstractResource):
urls = ["/api/pba/download/<string:filename>"]
"""
File download endpoint used by monkey to download user's PBA file

View File

@ -1,13 +1,12 @@
import logging
from http import HTTPStatus
import flask_restful
from flask import Response, make_response, request, send_file
from werkzeug.utils import secure_filename as sanitize_filename
from common.config_value_paths import PBA_LINUX_FILENAME_PATH, PBA_WINDOWS_FILENAME_PATH
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services import FileRetrievalError, IFileStorageService
from monkey_island.cc.services.config import ConfigService
@ -18,7 +17,7 @@ LINUX_PBA_TYPE = "PBAlinux"
WINDOWS_PBA_TYPE = "PBAwindows"
class FileUpload(flask_restful.Resource, IResource):
class FileUpload(AbstractResource):
"""
File upload endpoint used to send/receive Custom PBA files
"""

View File

@ -1,11 +1,9 @@
import flask_restful
from monkey_island.cc.database import mongo
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.services.config import ConfigService
class PropagationCredentials(flask_restful.Resource, IResource):
class PropagationCredentials(AbstractResource):
urls = ["/api/propagation-credentials/<string:guid>"]
def get(self, guid: str):

View File

@ -1,12 +1,11 @@
import flask_restful
from flask import jsonify
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.ransomware import ransomware_report
class RansomwareReport(flask_restful.Resource, IResource):
class RansomwareReport(AbstractResource):
urls = ["/api/report/ransomware"]
@jwt_required

View File

@ -1,12 +1,11 @@
import json
from typing import Sequence
import flask_restful
from botocore.exceptions import ClientError, NoCredentialsError
from flask import jsonify, make_response, request
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services import AWSService
from monkey_island.cc.services.aws import AWSCommandResults
@ -20,7 +19,7 @@ NO_CREDS_ERROR_FORMAT = (
)
class RemoteRun(flask_restful.Resource, IResource):
class RemoteRun(AbstractResource):
urls = ["/api/remote-monkey"]
def __init__(self, aws_service: AWSService):

View File

@ -1,11 +1,10 @@
import logging
import flask_restful
from flask import jsonify, make_response, request
from monkey_island.cc.database import mongo
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.database import Database
from monkey_island.cc.services.infection_lifecycle import get_completed_steps
from monkey_island.cc.services.utils.network_utils import local_ip_addresses
@ -13,7 +12,7 @@ from monkey_island.cc.services.utils.network_utils import local_ip_addresses
logger = logging.getLogger(__name__)
class Root(IResource, flask_restful.Resource):
class Root(AbstractResource):
urls = ["/api"]

View File

@ -1,11 +1,9 @@
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.reporting.report import ReportService
class SecurityReport(flask_restful.Resource, IResource):
class SecurityReport(AbstractResource):
urls = ["/api/report/security"]
@jwt_required

View File

@ -3,22 +3,21 @@ import logging
from datetime import datetime
import dateutil
import flask_restful
from flask import request
from monkey_island.cc.database import mongo
from monkey_island.cc.models.monkey import Monkey
from monkey_island.cc.models.telemetries import get_telemetry_by_query
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.telemetry.processing.processing import process_telemetry
logger = logging.getLogger(__name__)
class Telemetry(flask_restful.Resource, IResource):
class Telemetry(AbstractResource):
urls = ["/api/telemetry", "/api/telemetry/<string:monkey_guid>"]
@jwt_required

View File

@ -3,19 +3,18 @@ from datetime import datetime
import dateutil
import flask_pymongo
import flask_restful
from flask import request
from common.common_consts.telem_categories import TelemCategoryEnum
from monkey_island.cc.database import mongo
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.node import NodeService
logger = logging.getLogger(__name__)
class TelemetryFeed(flask_restful.Resource, IResource):
class TelemetryFeed(AbstractResource):
urls = ["/api/telemetry-feed"]
@jwt_required

View File

@ -1,15 +1,13 @@
import logging
import flask_restful
from common.version import get_version
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.services.version_update import VersionUpdateService
logger = logging.getLogger(__name__)
class VersionUpdate(flask_restful.Resource, IResource):
class VersionUpdate(AbstractResource):
urls = ["/api/version-update"]
def __init__(self):

View File

@ -1,15 +1,13 @@
import json
import flask_restful
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import (
MonkeyZTFindingService,
)
class ZeroTrustFindingEvent(flask_restful.Resource, IResource):
class ZeroTrustFindingEvent(AbstractResource):
urls = ["/api/zero-trust/finding-event/<string:finding_id>"]
@jwt_required

View File

@ -3,8 +3,8 @@ import http.client
import flask_restful
from flask import jsonify
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.resources.auth.auth import jwt_required
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService
from monkey_island.cc.services.zero_trust.zero_trust_report.pillar_service import PillarService
from monkey_island.cc.services.zero_trust.zero_trust_report.principle_service import (
@ -16,7 +16,7 @@ REPORT_DATA_FINDINGS = "findings"
REPORT_DATA_PRINCIPLES_STATUS = "principles"
class ZeroTrustReport(flask_restful.Resource, IResource):
class ZeroTrustReport(AbstractResource):
urls = ["/api/report/zero-trust/<string:report_data>"]
@jwt_required

View File

@ -1,14 +1,13 @@
import flask_restful
import pytest
from tests.common import StubDIContainer
from tests.unit_tests.monkey_island.conftest import mock_flask_resource_manager
from monkey_island.cc.app import FlaskDIWrapper
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
def get_mock_resource(name, urls):
class MockResource(flask_restful.Resource, IResource):
class MockResource(AbstractResource):
urls = []
def get(self, something=None):

View File

@ -8,7 +8,7 @@ import pytest
from flask import Flask
import monkey_island
from monkey_island.cc.resources.i_resource import IResource
from monkey_island.cc.resources.AbstractResource import AbstractResource
from monkey_island.cc.services.representations import output_json
@ -47,7 +47,7 @@ def mock_flask_resource_manager(container):
return flask_resource_manager
def get_url_for_resource(resource: IResource, **kwargs):
def get_url_for_resource(resource: AbstractResource, **kwargs):
chosen_url = None
for url in resource.urls:
if _get_url_keywords(url) == set(kwargs.keys()):