Пример #1
0
def auth_state(MockAuthClient, config, monkeypatch):
    # Mock the introspection first because that gets called as soon as we create
    # a TokenChecker
    client = MockAuthClient.return_value
    client.oauth2_token_introspect.return_value = (
        canned_responses.introspect_response()()
    )

    # Mock the dependent_tokens and list_groups functions bc they get used when
    # creating a GroupsClient
    client.oauth2_get_dependent_tokens.return_value = (
        canned_responses.dependent_token_response()()
    )
    monkeypatch.setattr(GroupsClient, "list_groups", canned_responses.groups_response())

    # Create a TokenChecker to be used to create a mocked auth_state object
    checker = TokenChecker(
        client_id=config["client_id"],
        client_secret=config["client_secret"],
        expected_scopes=config["expected_scopes"],
        expected_audience=config["expected_audience"],
    )
    auth_state = checker.check_token("NOT_A_TOKEN")

    # Reset the call count because check_token implicitly calls oauth2_token_introspect
    client.oauth2_token_introspect.call_count = 0

    # Mock out this AuthState instance's GroupClient
    # auth_state._groups_client = GroupsClient(authorizer=None)
    # auth_state._groups_client.list_groups = canned_responses.groups_response()
    return auth_state
def test_token_checker_bad_credentials():
    with pytest.raises(ConfigurationError):
        TokenChecker(
            client_id="bogus",
            client_secret="bogus",
            expected_scopes=("fakescope", ),
        )
Пример #3
0
def check_token(request: Request, checker: TokenChecker) -> AuthState:
    """
    Parses a Flask request to extract its bearer token.
    """
    access_token = request.headers.get("Authorization",
                                       "").strip().lstrip("Bearer ")
    auth_state = checker.check_token(access_token)
    return auth_state
Пример #4
0
    def _create_token_checker(self, setup_state: blueprints.BlueprintSetupState):
        app = setup_state.app
        provider_prefix = self.name.upper() + "_"
        client_id = app.config.get(provider_prefix + "CLIENT_ID")
        client_secret = app.config.get(provider_prefix + "CLIENT_SECRET")

        if not client_id or not client_secret:
            client_id = app.config.get("CLIENT_ID")
            client_secret = app.config.get("CLIENT_SECRET")

        scopes = [self.provider_description.globus_auth_scope]
        scopes.extend(self.additional_scopes)

        self.checker = TokenChecker(
            client_id=client_id,
            client_secret=client_secret,
            expected_scopes=scopes,
            expected_audience=self.globus_auth_client_name,
        )
Пример #5
0
# Flask setup
app = Flask(__name__)
app.config.from_mapping(**CONFIG)
app.url_map.strict_slashes = False

# Logging setup
logging.config.dictConfig(CONFIG["LOGGING"])
logger = logging.getLogger(__name__)

logger.info("\n\n==========CFDE Action Provider started==========\n")

# Globals specific to this instance
TBL = CONFIG["DYNAMO_TABLE"]
ROOT = "/"  # Segregate different APs by root path?
TOKEN_CHECKER = TokenChecker(CONFIG["GLOBUS_CC_APP"], CONFIG["GLOBUS_SECRET"],
                             [CONFIG["GLOBUS_SCOPE"]], CONFIG["GLOBUS_AUD"])

# Clean up environment
utils.clean_environment()
utils.initialize_dmo_table(CONFIG["DYNAMO_TABLE"])

#######################################
# Flask helpers
#######################################


@app.errorhandler(err.ApiError)
def handle_invalid_usage(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status
    return response
Пример #6
0
def add_action_routes_to_blueprint(
    blueprint: flask.Blueprint,
    client_id: str,
    client_secret: str,
    client_name: Optional[str],
    provider_description: ActionProviderDescription,
    action_run_callback: ActionRunType,
    action_status_callback: ActionStatusType,
    action_cancel_callback: ActionCancelType,
    action_release_callback: ActionReleaseType,
    action_log_callback: Optional[ActionLogType] = None,
    additional_scopes: Optional[List[str]] = None,
    action_enumeration_callback: ActionEnumerationType = None,
) -> None:
    """Add routes to a Flask Blueprint to implement the required operations of the Action
    Provider Interface: Introspect, Run, Status, Cancel and Release. The route handlers
    added to the blueprint perform basic functionality such as input validation and
    authorization checks where appropriate, and use the provided callbacks to implement
    the action provider specific functionality. See description of each callback below
    for a description of functionality performed prior to invoking the callback.

    **Parameters**

    ``blueprint`` (*Flask.Blueprint*) 
    A flask blueprint to which routes for the URL paths '/', '/run', '/status',
    '/cancel', and '/release' will be added. Optionally, (see below) '/log' will be added
    as well. The blueprint should define a ``url_prefix`` to define a root to the paths
    where these new paths will be added. In addition to the new URL paths, the blueprint
    will also have a custom JSONEncoder associated with it to aid in the serialization of
    data-types associated with these operations.

    ``client_id`` (*string*)
    A Globus Auth registered ``client_id`` which will be used when validating input
    request tokens.

    ``client_secret`` (*string*)
    A Globus Auth generated ``client_secret`` which will be used when validating input
    request tokens.

    ``client_name`` (*string*) Most commonly, this will be a None value. In the rare,
    legacy case where a name has been associated with a client_id, it can be provided
    here. If you are not aware of a name associated with your client_id, it most likely
    doesn't have one and the value should be None. This will be passed to the
    (:class:`TokenChecker<globus_action_provider_tools.authentication>`) as the
    `expected_audience`.

    ``provider_description`` (:class:`ActionProviderDescription\
    <globus_action_provider_tools.data_types>`)
    A structure describing the provider to be returned by the provider introspection
    operation (`GET /`). Some fields are also used in processing requests: the
    `input_schema` field is used to validate the `body` of incoming action requests on
    the `/run` operation. The `globus_auth_scope` value is used to validate the incoming
    tokens on all requests. The `visible_to` and `runnable_by` lists are used to
    authorization operations on the introspect (GET '/') and run (POST '/run') operations
    respectively. The `log_supported` field should be `True` only if the
    `action_log_callback` parameter is provided a value.

    ``action_run_callback`` (* Callable[[ActionRequest, AuthState], Union[ActionStatus,
    Tuple[ActionStatus, int]]] *)
    A function which will be called when an action /run invocation is called. Prior to
    invoking the callback, the handler will validate the input conforms to the Action
    Provider defined request format *and* that the input `body` matches the
    `input_schema` defined in the `provider_description`. It will also authorize the
    caller against the `runnable_by` property of the `provider_description`. In the case
    of any validation or authorization errors, the corresponding werkzeug defined
    exception will be raised. When validation and authorization succeed, the callback
    will be invoked providing the `ActionRequest` structure corresponding to the request
    and the authorization state (`AuthState`) of the caller. The callback should return
    an `ActionStatus` value to be returned on the invocation. Optionally, a status
    integer can be added to the return (making the return a (ActionStatus, int) tuple)
    which defines the HTTP status code to be returned. This is useful in the case where
    an existing request with the same id and body are seen which should return a 200 HTTP
    status rather than the normal 201 HTTP status (which is the default when the status
    code is not returned).

    """
    if additional_scopes:
        all_accepted_scopes = additional_scopes + [
            provider_description.globus_auth_scope
        ]
    else:
        all_accepted_scopes = [provider_description.globus_auth_scope]

    checker = TokenChecker(
        client_id=client_id,
        client_secret=client_secret,
        expected_scopes=all_accepted_scopes,
        expected_audience=client_name,
    )

    blueprint.json_encoder = ActionProviderJsonEncoder
    input_body_validator = get_input_body_validator(provider_description)
    blueprint.register_error_handler(Exception, blueprint_error_handler)

    @blueprint.route("/", methods=["GET"], strict_slashes=False)
    def action_introspect() -> ViewReturn:
        auth_state = check_token(request, checker)
        if not auth_state.check_authorization(
            provider_description.visible_to,
            allow_public=True,
            allow_all_authenticated_users=True,
        ):
            raise ActionNotFound
        return jsonify(provider_description), 200

    @blueprint.route("/actions", methods=["POST"])
    @blueprint.route("/run", methods=["POST"])
    def action_run() -> ViewReturn:
        auth_state = check_token(request, checker)
        if not auth_state.check_authorization(
            provider_description.runnable_by, allow_all_authenticated_users=True
        ):
            log.info(f"Unauthorized call to action run, errors: {auth_state.errors}")
            raise UnauthorizedRequest
        if blueprint.url_prefix:
            request.path = request.path.lstrip(blueprint.url_prefix)
            if request.url_rule is not None:
                request.url_rule.rule = request.url_rule.rule.lstrip(
                    blueprint.url_prefix
                )

        action_request = validate_input(
            request.get_json(force=True), input_body_validator
        )

        # It's possible the user will attempt to make a malformed ActionStatus -
        # pydantic won't like that. So log and handle the error with a 500
        try:
            status = action_run_callback(action_request, auth_state)
        except ValidationError as ve:
            log.error(
                f"ActionProvider attempted to create a non-conformant ActionStatus"
                f" in {action_run_callback.__name__}: {ve.errors()}"
            )
            raise ActionProviderError

        return action_status_return_to_view_return(status, 202)

    @blueprint.route("/<string:action_id>/status", methods=["GET"])
    @blueprint.route("/actions/<string:action_id>", methods=["GET"])
    def action_status(action_id: str) -> ViewReturn:
        auth_state = check_token(request, checker)
        try:
            status = action_status_callback(action_id, auth_state)
        except ValidationError as ve:
            log.error(
                f"ActionProvider attempted to create a non-conformant ActionStatus"
                f" in {action_status_callback.__name__}: {ve.errors()}"
            )
            raise ActionProviderError
        return action_status_return_to_view_return(status, 200)

    @blueprint.route("/<string:action_id>/cancel", methods=["POST"])
    @blueprint.route("/actions/<string:action_id>/cancel", methods=["POST"])
    def action_cancel(action_id: str) -> ViewReturn:
        auth_state = check_token(request, checker)
        try:
            status = action_cancel_callback(action_id, auth_state)
        except ValidationError as ve:
            log.error(
                f"ActionProvider attempted to create a non-conformant ActionStatus"
                f" in {action_cancel_callback.__name__}: {ve.errors()}"
            )
            raise ActionProviderError
        return action_status_return_to_view_return(status, 200)

    @blueprint.route("/<string:action_id>/release", methods=["POST"])
    @blueprint.route("/actions/<string:action_id>", methods=["DELETE"])
    def action_release(action_id: str) -> ViewReturn:
        auth_state = check_token(request, checker)
        try:
            status = action_release_callback(action_id, auth_state)
        except ValidationError as ve:
            log.error(
                f"ActionProvider attempted to create a non-conformant ActionStatus"
                f" in {action_release_callback.__name__}: {ve.errors()}"
            )
            raise ActionProviderError
        return action_status_return_to_view_return(status, 200)

    if action_log_callback is not None:

        @blueprint.route("/actions/<string:action_id>/log", methods=["GET"])
        @blueprint.route("/<string:action_id>/log", methods=["GET"])
        def action_log(action_id: str) -> ViewReturn:
            auth_state = check_token(request, checker)
            return jsonify({"log": "message"}), 200

    if action_enumeration_callback is not None:

        @blueprint.route("/actions", methods=["GET"])
        def action_enumeration():
            auth_state = check_token(request, checker)

            valid_statuses = set(e.name.casefold() for e in ActionStatusValue)
            statuses = parse_query_args(
                request,
                arg_name="status",
                default_value="active",
                valid_vals=valid_statuses,
            )
            statuses = query_args_to_enum(statuses, ActionStatusValue)
            roles = parse_query_args(
                request,
                arg_name="roles",
                default_value="creator_id",
                valid_vals={"creator_id", "monitor_by", "manage_by"},
            )
            query_params = {"statuses": statuses, "roles": roles}
            return jsonify(action_enumeration_callback(auth_state, query_params)), 200
Пример #7
0
from globus_action_provider_tools.authentication import TokenChecker
from globus_action_provider_tools.authorization import (
    authorize_action_access_or_404,
    authorize_action_management_or_404,
)
from globus_action_provider_tools.data_types import (
    ActionProviderDescription,
    ActionProviderJsonEncoder,
    ActionStatus,
    ActionStatusValue,
)
from globus_action_provider_tools.flask import flask_validate_request

app = Flask(__name__)

token_checker = TokenChecker(config.client_id, config.client_secret,
                             [config.our_scope], config.token_audience)

COMPLETE_STATES = (ActionStatusValue.SUCCEEDED, ActionStatusValue.FAILED)
INCOMPLETE_STATES = (ActionStatusValue.ACTIVE, ActionStatusValue.INACTIVE)

with open(
        os.path.join(os.path.dirname(os.path.abspath(__file__)),
                     "schema.json")) as f:
    schema = json.load(f)

app.json_encoder = ActionProviderJsonEncoder


@app.errorhandler(err.ApiError)
def handle_invalid_usage(error) -> Response:
    response = jsonify(error.to_dict())