Skip to content

Commit

Permalink
Do not use core Airflow Flask related resources in FAB provider (pack…
Browse files Browse the repository at this point in the history
…age `api_connexion`)
  • Loading branch information
vincbeck committed Jan 7, 2025
1 parent 1dcc29a commit dd8379f
Show file tree
Hide file tree
Showing 13 changed files with 471 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@
from marshmallow import ValidationError
from sqlalchemy import asc, desc, func, select

from airflow.api_connexion.exceptions import AlreadyExists, BadRequest, NotFound
from airflow.api_connexion.parameters import check_limit, format_parameters
from airflow.api_connexion.security import requires_access_custom_view
from airflow.api_fastapi.app import get_auth_manager
from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager
from airflow.providers.fab.auth_manager.models import Action, Role
Expand All @@ -37,11 +34,14 @@
role_collection_schema,
role_schema,
)
from airflow.providers.fab.www.api_connexion.exceptions import AlreadyExists, BadRequest, NotFound
from airflow.providers.fab.www.api_connexion.parameters import check_limit, format_parameters
from airflow.providers.fab.www.api_connexion.security import requires_access_custom_view
from airflow.security import permissions

if TYPE_CHECKING:
from airflow.api_connexion.types import APIResponse, UpdateMask
from airflow.providers.fab.auth_manager.security_manager.override import FabAirflowSecurityManagerOverride
from airflow.providers.fab.www.api_connexion.types import APIResponse, UpdateMask


def _check_action_and_resource(sm: FabAirflowSecurityManagerOverride, perms: list[tuple[str, str]]) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
from sqlalchemy import asc, desc, func, select
from werkzeug.security import generate_password_hash

from airflow.api_connexion.exceptions import AlreadyExists, BadRequest, NotFound, Unknown
from airflow.api_connexion.parameters import check_limit, format_parameters
from airflow.api_connexion.security import requires_access_custom_view
from airflow.api_fastapi.app import get_auth_manager
from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager
from airflow.providers.fab.auth_manager.models import User
Expand All @@ -37,11 +34,14 @@
user_collection_schema,
user_schema,
)
from airflow.providers.fab.www.api_connexion.exceptions import AlreadyExists, BadRequest, NotFound, Unknown
from airflow.providers.fab.www.api_connexion.parameters import check_limit, format_parameters
from airflow.providers.fab.www.api_connexion.security import requires_access_custom_view
from airflow.security import permissions

if TYPE_CHECKING:
from airflow.api_connexion.types import APIResponse, UpdateMask
from airflow.providers.fab.auth_manager.models import Role
from airflow.providers.fab.www.api_connexion.types import APIResponse, UpdateMask


@requires_access_custom_view("GET", permissions.RESOURCE_USER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
from marshmallow import Schema, fields
from marshmallow_sqlalchemy import SQLAlchemySchema, auto_field

from airflow.api_connexion.parameters import validate_istimezone
from airflow.providers.fab.auth_manager.models import User
from airflow.providers.fab.auth_manager.schemas.role_and_permission_schema import RoleSchema
from airflow.providers.fab.www.api_connexion.parameters import validate_istimezone


class UserCollectionItemSchema(SQLAlchemySchema):
Expand Down
17 changes: 17 additions & 0 deletions providers/src/airflow/providers/fab/www/api_connexion/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
197 changes: 197 additions & 0 deletions providers/src/airflow/providers/fab/www/api_connexion/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from http import HTTPStatus
from typing import TYPE_CHECKING, Any

import werkzeug
from connexion import FlaskApi, ProblemException, problem

from airflow.utils.docs import get_docs_url

if TYPE_CHECKING:
import flask

doc_link = get_docs_url("stable-rest-api-ref.html")

EXCEPTIONS_LINK_MAP = {
400: f"{doc_link}#section/Errors/BadRequest",
404: f"{doc_link}#section/Errors/NotFound",
405: f"{doc_link}#section/Errors/MethodNotAllowed",
401: f"{doc_link}#section/Errors/Unauthenticated",
409: f"{doc_link}#section/Errors/AlreadyExists",
403: f"{doc_link}#section/Errors/PermissionDenied",
500: f"{doc_link}#section/Errors/Unknown",
}


def common_error_handler(exception: BaseException) -> flask.Response:
"""Use to capture connexion exceptions and add link to the type field."""
if isinstance(exception, ProblemException):
link = EXCEPTIONS_LINK_MAP.get(exception.status)
if link:
response = problem(
status=exception.status,
title=exception.title,
detail=exception.detail,
type=link,
instance=exception.instance,
headers=exception.headers,
ext=exception.ext,
)
else:
response = problem(
status=exception.status,
title=exception.title,
detail=exception.detail,
type=exception.type,
instance=exception.instance,
headers=exception.headers,
ext=exception.ext,
)
else:
if not isinstance(exception, werkzeug.exceptions.HTTPException):
exception = werkzeug.exceptions.InternalServerError()

response = problem(title=exception.name, detail=exception.description, status=exception.code)

return FlaskApi.get_response(response)


class NotFound(ProblemException):
"""Raise when the object cannot be found."""

def __init__(
self,
title: str = "Not Found",
detail: str | None = None,
headers: dict | None = None,
**kwargs: Any,
) -> None:
super().__init__(
status=HTTPStatus.NOT_FOUND,
type=EXCEPTIONS_LINK_MAP[404],
title=title,
detail=detail,
headers=headers,
**kwargs,
)


class BadRequest(ProblemException):
"""Raise when the server processes a bad request."""

def __init__(
self,
title: str = "Bad Request",
detail: str | None = None,
headers: dict | None = None,
**kwargs: Any,
) -> None:
super().__init__(
status=HTTPStatus.BAD_REQUEST,
type=EXCEPTIONS_LINK_MAP[400],
title=title,
detail=detail,
headers=headers,
**kwargs,
)


class Unauthenticated(ProblemException):
"""Raise when the user is not authenticated."""

def __init__(
self,
title: str = "Unauthorized",
detail: str | None = None,
headers: dict | None = None,
**kwargs: Any,
):
super().__init__(
status=HTTPStatus.UNAUTHORIZED,
type=EXCEPTIONS_LINK_MAP[401],
title=title,
detail=detail,
headers=headers,
**kwargs,
)


class PermissionDenied(ProblemException):
"""Raise when the user does not have the required permissions."""

def __init__(
self,
title: str = "Forbidden",
detail: str | None = None,
headers: dict | None = None,
**kwargs: Any,
) -> None:
super().__init__(
status=HTTPStatus.FORBIDDEN,
type=EXCEPTIONS_LINK_MAP[403],
title=title,
detail=detail,
headers=headers,
**kwargs,
)


class Conflict(ProblemException):
"""Raise when there is some conflict."""

def __init__(
self,
title="Conflict",
detail: str | None = None,
headers: dict | None = None,
**kwargs: Any,
):
super().__init__(
status=HTTPStatus.CONFLICT,
type=EXCEPTIONS_LINK_MAP[409],
title=title,
detail=detail,
headers=headers,
**kwargs,
)


class AlreadyExists(Conflict):
"""Raise when the object already exists."""


class Unknown(ProblemException):
"""Returns a response body and status code for HTTP 500 exception."""

def __init__(
self,
title: str = "Internal Server Error",
detail: str | None = None,
headers: dict | None = None,
**kwargs: Any,
) -> None:
super().__init__(
status=HTTPStatus.INTERNAL_SERVER_ERROR,
type=EXCEPTIONS_LINK_MAP[500],
title=title,
detail=detail,
headers=headers,
**kwargs,
)
Loading

0 comments on commit dd8379f

Please sign in to comment.