-
Notifications
You must be signed in to change notification settings - Fork 16.4k
UI Rest API authentication #42019
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
UI Rest API authentication #42019
Changes from all commits
7f0ffa2
41dd741
b5eb03a
c666aa7
d5eba9b
4f572ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # 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 typing import TYPE_CHECKING | ||
|
|
||
| from airflow.utils.session import create_session | ||
|
|
||
| if TYPE_CHECKING: | ||
| from sqlalchemy.orm import Session | ||
|
|
||
|
|
||
| async def get_session() -> Session: | ||
| """ | ||
| Dependency for providing a session. | ||
|
|
||
| For non route function please use the use the :class:`airflow.utils.session.provide_session` decorator. | ||
|
|
||
| Example usage: | ||
|
|
||
| .. code:: python | ||
|
|
||
| @router.get("/your_path") | ||
| def your_route(session: Annotated[Session, Depends(get_session)]): | ||
| pass | ||
| """ | ||
| with create_session() as session: | ||
| yield session |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # 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 typing import Callable, cast | ||
|
|
||
| from fastapi import Depends, HTTPException, Request | ||
| from fastapi.security import HTTPBasic, HTTPBasicCredentials | ||
| from typing_extensions import Annotated | ||
|
|
||
| from airflow.auth.managers.base_auth_manager import ResourceMethod | ||
| from airflow.auth.managers.models.base_user import BaseUser | ||
| from airflow.auth.managers.models.resource_details import DagAccessEntity, DagDetails, DatasetDetails | ||
| from airflow.providers.fab.auth_manager.api.auth.backend.basic_auth import auth_current_user | ||
| from airflow.providers.fab.auth_manager.models import User | ||
| from airflow.www.extensions.init_auth_manager import get_auth_manager | ||
|
|
||
| security = HTTPBasic() | ||
|
|
||
|
|
||
| def method(request: Request) -> ResourceMethod: | ||
| return cast(ResourceMethod, request.method) | ||
|
|
||
|
|
||
| def check_authentication( | ||
| credentials: Annotated[HTTPBasicCredentials, Depends(security)], | ||
| ) -> User | None: | ||
| """Check that the request has valid authorization information.""" | ||
| # TODO: Handle other auth backends when they support FastAPI. (session, kerberos, etc.) | ||
| user = auth_current_user(credentials) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of relying on basic auth (that is part of FAB auth manager), I would do it the same way as in https://github.com/apache/airflow/blob/main/airflow/api_connexion/security.py#L44
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not possible at the moment. This is what I did at first, exact same code, the problem is in the implementation, none of the backends are working with fastapi for the same reason, they all need a flask app context to access
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cf this comment for a more detailed example with the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR as been merged. Just removing the |
||
| if user is not None: | ||
| return user | ||
|
|
||
| # since this handler only checks authentication, not authorization, | ||
| # we should always return 401 | ||
| raise HTTPException(401, headers={"WWW-Authenticate": "Basic"}) | ||
|
|
||
|
|
||
| def _requires_access( | ||
| *, | ||
| is_authorized_callback: Callable[[], bool], | ||
| ) -> None: | ||
| if not is_authorized_callback(): | ||
| raise HTTPException(403, "Forbidden") | ||
|
|
||
|
|
||
| def requires_access_dataset( | ||
| method: Annotated[ResourceMethod, Depends(method)], | ||
| uri: str | None = None, | ||
| user: Annotated[BaseUser | None, Depends(check_authentication)] = None, | ||
| ) -> None: | ||
| _requires_access( | ||
| is_authorized_callback=lambda: get_auth_manager().is_authorized_dataset( | ||
| user=user, | ||
| method=method, | ||
| details=DatasetDetails(uri=uri), | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def requires_access_dag(access_entity: DagAccessEntity | None = None) -> Callable: | ||
| def inner( | ||
| method: Annotated[ResourceMethod, Depends(method)], | ||
| dag_id: str | None = None, | ||
| user: Annotated[BaseUser | None, Depends(check_authentication)] = None, | ||
| ) -> None: | ||
| def callback(): | ||
| access = get_auth_manager().is_authorized_dag( | ||
| method=method, access_entity=access_entity, details=DagDetails(id=dag_id), user=user | ||
| ) | ||
|
|
||
| # ``access`` means here: | ||
| # - if a DAG id is provided (``dag_id`` not None): is the user authorized to access this DAG | ||
| # - if no DAG id is provided: is the user authorized to access all DAGs | ||
| if dag_id or access or access_entity: | ||
| return access | ||
|
|
||
| # No DAG id is provided, the user is not authorized to access all DAGs and authorization is done | ||
| # on DAG level | ||
| # If method is "GET", return whether the user has read access to any DAGs | ||
| # If method is "PUT", return whether the user has edit access to any DAGs | ||
| return (method == "GET" and any(get_auth_manager().get_permitted_dag_ids(methods=["GET"]))) or ( | ||
| method == "PUT" and any(get_auth_manager().get_permitted_dag_ids(methods=["PUT"])) | ||
| ) | ||
pierrejeambrun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| _requires_access( | ||
| is_authorized_callback=callback, | ||
| ) | ||
|
|
||
| return inner | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question. Is it entirely true for Airflow 3? Do we want to base all our authentication on Flask and have to create a Flask App (cc: @vincbeck ?). I do not think there is (maybe due to compatibility but can be removed) any requirement to use flask in our Auth Manager?
Not that I have another proposal there - as I have a little knowledge about flask/asgi/wsgi etc - but I am not sure if we really need to use Flask. Flask is essentially synchronous and writing APIs is not really the main goal of Flask (producing HTML output is). But if we get rid of Flask we could be free to use sync/async requests as we want and some of the problems where joining Starlette, ASGI, gunicorn, uvicorn and all the complexity resulting from that could be avoided.
If we are going fast-api-first and async built-in, getting rid of flask should only bring benefits IMHO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Maybe I do not understand the tight-coupling here) - but I don't think we are in any way tightly coupled with flask (other than compatibility requirements for the old plugins).
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes indeed we really do not want to have Flask around anymore I believe when we release airflow 3. There is no reason to if all of our APIs are on FastAPI.
Yes FastAPI has async support natively, but we cannot have that 'freely' now because most of the db utility code of airflow, for instance how to create sessions and provide them via a decorator, how to create the db engine, context managers etc are not yet written in an async way, so we would need to write that. (I think that would be part of AIP-70). It is definitely the endgoal to be fully async for a FastAPI. Maybe we don't need AIP-70 for that but it's a significant effort to rewrite all of those and not critical for the POC to work. Also regarding the migration of endpionts from the old api to the new api it will be a bit easier if both are sync, basically that's close to a copy and past (i am exaggerating a bit but you get the idea). More code will need to be adapted if the new endpoints are async while the old ones are not. Maybe it is better to do that in a 'second' step, when everything is working in fastapi synchronously.
Those are the problem at the moment I believe. At multiple places in our backend_auth, permission code, etc.. etc..., we use the flask app context to retrieve global things such as the connected user, the sessions, the current request. Those are not available when running FastAPI, to give some exemples:
FABAuthManageruses flask_login.current_user, connexion.FlaskAPI,FabAirflowSecurityManagerOverridehas calls to flask libraries to handle jwt or openid or access directly the flaskgglobal object to retrieve the user. Usesflask_login.LoginManager,flask.flashor thesessionglobal object.AnonymousUseris using the flask.current_app() and aflask_loginmixin.Those are just some exemples, there a plenty more in the code, maybe I am misunderstanding something and Vincent/Jed will have more information on that and we can simply write another SecurityManager / AuthManager agnostic of flask but I am not sure yet what is the best approach.
Also just to mention that FastAPI and Flask work in two different paradigm. Flask uses global object and application context accessor basically via importing
gorcurrent_userand using them where you need them in the views / provider / utility / managers. In opposition FastAPI uses dependency injection, where function signature declares its dependency and FastAPI will inject them at runtime. There might be some refactoring involved due to this shift.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is definitely going to have to be some decoupling/refactoring anyways. For example, auth managers expect an appbuilder today, and it's already on my "must change" list for AIP-79 :)
But yes, FAB / our security manager / auth managers, it's all pretty intertwined and messy today. I believe we can solve it in 3, but in the meantime I think initing like this is okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have quickly looked at the code, I will look deeper and provide inline comments to help but overall:
FABAuthManager,FabAirflowSecurityManagerOverride,AnonymousUserare all specific to FAB auth manager.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont want to block your PR though, this is not blocking and can be solved later but this is something we definitely need to figure out
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do not rely on a session auth, we need the front end to pass a Basic auth header. Basically the base 64 encoded username and password on each request. There are utilities in the front end to do that. And maybe store the username/password in cookies in the frontend. 🤔. There are definitely solutions for 'basic auth' workflow on the front end but this is just for development indeed.
For production I am not even sure that we want session cookie based auth for a modern FastAPI app. JWT Bearer might seem more appropriate. But we do not have that kind of auth backend available yet ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I think we need to align on the authentication storage. I assumed we were going to use a session but we might want to take a different approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a discussion with @bbovenzi at the Airflow summit, it seems we are aiming toward an identification using JWT token and session as storage for the token
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks for the heads up, keep me posted if there is some work done in that direction that will allow me to move this PR forward. (but no hurry for now it can stay here and we move on without auth for development)