This is an automated email from the ASF dual-hosted git repository.
vincbeck pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new d5ca289f414 Add CLI login discovery to auth managers (#75672)
d5ca289f414 is described below
commit d5ca289f41493b1cb5e64c7e0e0b742bb9719a79
Author: Baptiste Gouhoury <[email protected]>
AuthorDate: Wed Oct 28 21:58:41 2026 +0100
Add CLI login discovery to auth managers (#75672)
airflowctl cannot tell which login flows a server supports before it
tries one, so an unsupported flow surfaces as a 404 or, for browser
logins, a silent timeout. Auth managers can now advertise the flows
they offer to command-line clients, using RFC 8414 field names, so
clients can choose a supported flow or fail with a clear message.
The flows must not depend on a specific auth manager, so the contract
each advertised grant follows is documented in core. Any auth manager
can then offer CLI login without changes to airflowctl.
Signed-off-by: Baptiste Gouhoury <[email protected]>
---
.../docs/core-concepts/auth-manager/index.rst | 25 ++++++++++
airflow-core/docs/security/api_permissions_ref.rst | 4 ++
.../api_fastapi/auth/managers/base_auth_manager.py | 11 ++++-
.../src/airflow/api_fastapi/common/types.py | 21 +++++++-
.../api_fastapi/core_api/datamodels/auth.py | 29 +++++++++++
.../core_api/openapi/v2-rest-api-generated.yaml | 52 ++++++++++++++++++++
.../api_fastapi/core_api/routes/public/auth.py | 14 ++++++
.../src/airflow/ui/openapi-gen/queries/common.ts | 4 ++
.../ui/openapi-gen/queries/ensureQueryData.ts | 7 +++
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 7 +++
.../src/airflow/ui/openapi-gen/queries/queries.ts | 7 +++
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 7 +++
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 56 ++++++++++++++++++++++
.../ui/openapi-gen/requests/services.gen.ts | 15 +++++-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 23 +++++++++
.../auth/managers/test_base_auth_manager.py | 5 +-
.../core_api/routes/public/test_auth.py | 47 +++++++++++++++++-
.../api_fastapi/core_api/routes/test_routes.py | 1 +
.../src/airflowctl/api/datamodels/generated.py | 12 +++++
providers/fab/docs/auth-manager/access-control.rst | 4 ++
20 files changed, 346 insertions(+), 5 deletions(-)
diff --git a/airflow-core/docs/core-concepts/auth-manager/index.rst
b/airflow-core/docs/core-concepts/auth-manager/index.rst
index 7431d84c796..f853cd38fa0 100644
--- a/airflow-core/docs/core-concepts/auth-manager/index.rst
+++ b/airflow-core/docs/core-concepts/auth-manager/index.rst
@@ -106,11 +106,36 @@ Authentication related methods
* ``get_url_login``: Return the URL the user is redirected to for signing in.
* ``get_url_logout``: Return the URL the user is redirected to when logging
out. This is an optional method,
this redirection is usually needed to invalidate resources when logging out,
such as a session.
+* ``get_cli_login_methods``: Return the login methods offered to command-line
clients such as ``airflowctl``.
+ This is an optional method; by default, no method is advertised. See
:ref:`auth-manager-cli-login`.
* ``serialize_user``: Serialize a user instance to a dict. This dict is the
actual content of the JWT token.
It should contain all the information needed to identify the user and make
an authorization request.
* ``deserialize_user``: Create a user instance from a dict. The dict is the
payload of the JWT token.
This is the same dict returned by ``serialize_user``.
+.. _auth-manager-cli-login:
+
+Login from command-line clients
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Before logging in, command-line clients such as ``airflowctl`` read ``GET
/api/v2/auth/cli/metadata``, an
+unauthenticated route that returns ``get_cli_login_methods``. It uses RFC 8414
field names, except that endpoints
+are absolute paths, resolved against the API server URL. They include the path
of ``[api] base_url``, if any.
+An empty ``grant_types_supported`` means no CLI login is offered.
+
+Each advertised grant must follow the contract below, so that clients work
with any auth manager. A successful call
+to ``token_endpoint`` returns ``201`` with ``{"access_token": ...}``, valid
for ``[api_auth] jwt_cli_expiration_time``.
+
+* ``password``: the client sends ``username`` and ``password``. This is the
default when ``grant_type`` is omitted.
+* ``authorization_code``: the browser opens ``authorization_endpoint`` with
``redirect_uri`` (``http`` loopback
+ only), ``state`` and a required ``S256`` ``code_challenge``. The client then
sends ``code``, ``code_verifier`` and
+ ``redirect_uri``.
+* ``urn:ietf:params:oauth:grant-type:device_code``:
``device_authorization_endpoint`` returns the RFC 8628 fields.
+ The client polls with ``device_code``, and gets ``400`` with
``authorization_pending`` or ``slow_down`` until the
+ user approves.
+* ``refresh_token``: the client sends the current CLI token as ``token``.
Expired or revoked tokens are refused,
+ and the new token never expires later than the old one.
+
Authorization related methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/airflow-core/docs/security/api_permissions_ref.rst
b/airflow-core/docs/security/api_permissions_ref.rst
index c4997864f34..1723b8ffd72 100644
--- a/airflow-core/docs/security/api_permissions_ref.rst
+++ b/airflow-core/docs/security/api_permissions_ref.rst
@@ -110,6 +110,10 @@ source code so it stays up to date as endpoints are added
or changed.
- ``/api/v2/assets/{asset_id}/state-store/{key}``
- ``Asset``
- ``PUT``
+ * - ``GET``
+ - ``/api/v2/auth/cli/metadata``
+ - ``Public``
+ - ``No Airflow permission required``
* - ``GET``
- ``/api/v2/auth/login``
- ``Public``
diff --git
a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
index 8b2302393a0..05743a58688 100644
--- a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
+++ b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
@@ -46,7 +46,7 @@ from airflow.api_fastapi.auth.tokens import (
get_sig_validation_args,
get_signing_args,
)
-from airflow.api_fastapi.common.types import ExtraMenuItem, MenuItem
+from airflow.api_fastapi.common.types import CliLoginMethods, ExtraMenuItem,
MenuItem
from airflow.configuration import conf
from airflow.exceptions import RemovedInAirflow4Warning
from airflow.models import Connection, DagModel, Pool, Variable
@@ -237,6 +237,15 @@ class BaseAuthManager(Generic[T], LoggingMixin,
metaclass=ABCMeta):
"""
return None
+ def get_cli_login_methods(self) -> CliLoginMethods:
+ """
+ Return the login methods offered to command-line clients such as
airflowctl.
+
+ Clients read them before logging in, so they can tell which flows the
server supports.
+ By default, no method is advertised.
+ """
+ return CliLoginMethods()
+
def refresh_user(self, *, user: T) -> T | None:
"""
Refresh the user if needed.
diff --git a/airflow-core/src/airflow/api_fastapi/common/types.py
b/airflow-core/src/airflow/api_fastapi/common/types.py
index 251a15071b4..38d0c68f90a 100644
--- a/airflow-core/src/airflow/api_fastapi/common/types.py
+++ b/airflow-core/src/airflow/api_fastapi/common/types.py
@@ -17,7 +17,7 @@
from __future__ import annotations
import re
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum
from typing import Annotated, Literal
@@ -111,6 +111,25 @@ class ExtraMenuItem:
href: str
+@dataclass
+class CliLoginMethods:
+ """
+ Login methods an auth manager offers to command-line clients such as
airflowctl.
+
+ Field names follow the OAuth 2.0 Authorization Server Metadata (RFC 8414).
An empty
+ ``grant_types_supported`` means the auth manager offers no login method to
these clients.
+ Unlike RFC 8414, endpoints are absolute paths, such as
``/auth/token/cli``, that clients
+ resolve against the API server URL, because the auth manager may not know
the public URL
+ clients use to reach the API server. They include the path of ``[api]
base_url``, if any.
+ """
+
+ grant_types_supported: list[str] = field(default_factory=list)
+ code_challenge_methods_supported: list[str] = field(default_factory=list)
+ authorization_endpoint: str | None = None
+ device_authorization_endpoint: str | None = None
+ token_endpoint: str | None = None
+
+
class MenuItem(Enum):
"""Define all menu items defined in the menu."""
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/auth.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/auth.py
new file mode 100644
index 00000000000..f1f6276ed1b
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/auth.py
@@ -0,0 +1,29 @@
+# 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 airflow.api_fastapi.core_api.base import BaseModel
+
+
+class CliLoginMetadataResponse(BaseModel):
+ """Login methods the auth manager offers to command-line clients, named as
in RFC 8414."""
+
+ grant_types_supported: list[str]
+ code_challenge_methods_supported: list[str]
+ authorization_endpoint: str | None
+ device_authorization_endpoint: str | None
+ token_endpoint: str | None
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 7e99a405b68..ac426aa2ab2 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -12426,6 +12426,21 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ /api/v2/auth/cli/metadata:
+ get:
+ tags:
+ - Login
+ summary: Get Cli Login Metadata
+ description: Return the login methods the AuthManager configured offers
to command-line
+ clients.
+ operationId: get_cli_login_metadata
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CliLoginMetadataResponse'
/api/v2/auth/logout:
get:
tags:
@@ -14261,6 +14276,43 @@ components:
type: object
title: ClearTaskInstancesBody
description: Request body for Clear Task Instances endpoint.
+ CliLoginMetadataResponse:
+ properties:
+ grant_types_supported:
+ items:
+ type: string
+ type: array
+ title: Grant Types Supported
+ code_challenge_methods_supported:
+ items:
+ type: string
+ type: array
+ title: Code Challenge Methods Supported
+ authorization_endpoint:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Authorization Endpoint
+ device_authorization_endpoint:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Device Authorization Endpoint
+ token_endpoint:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Token Endpoint
+ type: object
+ required:
+ - grant_types_supported
+ - code_challenge_methods_supported
+ - authorization_endpoint
+ - device_authorization_endpoint
+ - token_endpoint
+ title: CliLoginMetadataResponse
+ description: Login methods the auth manager offers to command-line
clients,
+ named as in RFC 8414.
Config:
properties:
sections:
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py
index 486ccb7c451..42b8ba10301 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py
@@ -26,6 +26,7 @@ from fastapi.security import HTTPAuthorizationCredentials
from airflow.api_fastapi.app import get_cookie_path
from airflow.api_fastapi.auth.managers.base_auth_manager import
COOKIE_NAME_JWT_TOKEN
from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.core_api.datamodels.auth import
CliLoginMetadataResponse
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
from airflow.api_fastapi.core_api.security import (
AuthManagerDep,
@@ -60,6 +61,19 @@ def login(request: Request, auth_manager: AuthManagerDep,
next: None | str = Non
return RedirectResponse(login_url)
+@auth_router.get("/cli/metadata")
+def get_cli_login_metadata(auth_manager: AuthManagerDep) ->
CliLoginMetadataResponse:
+ """Return the login methods the AuthManager configured offers to
command-line clients."""
+ methods = auth_manager.get_cli_login_methods()
+ return CliLoginMetadataResponse(
+ grant_types_supported=methods.grant_types_supported,
+
code_challenge_methods_supported=methods.code_challenge_methods_supported,
+ authorization_endpoint=methods.authorization_endpoint,
+ device_authorization_endpoint=methods.device_authorization_endpoint,
+ token_endpoint=methods.token_endpoint,
+ )
+
+
@auth_router.get(
"/logout",
responses=create_openapi_http_exception_doc([status.HTTP_307_TEMPORARY_REDIRECT]),
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index 5170bea5453..6af9fa923c4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -1089,6 +1089,10 @@ export const useLoginServiceLoginKey =
"LoginServiceLogin";
export const UseLoginServiceLoginKeyFn = ({ next }: {
next?: string;
} = {}, queryKey?: Array<unknown>) => [useLoginServiceLoginKey, ...(queryKey
?? [{ next }])];
+export type LoginServiceGetCliLoginMetadataDefaultResponse =
Awaited<ReturnType<typeof LoginService.getCliLoginMetadata>>;
+export type LoginServiceGetCliLoginMetadataQueryResult<TData =
LoginServiceGetCliLoginMetadataDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
+export const useLoginServiceGetCliLoginMetadataKey =
"LoginServiceGetCliLoginMetadata";
+export const UseLoginServiceGetCliLoginMetadataKeyFn = (queryKey?:
Array<unknown>) => [useLoginServiceGetCliLoginMetadataKey, ...(queryKey ?? [])];
export type LoginServiceLogoutDefaultResponse = Awaited<ReturnType<typeof
LoginService.logout>>;
export type LoginServiceLogoutQueryResult<TData =
LoginServiceLogoutDefaultResponse, TError = unknown> = UseQueryResult<TData,
TError>;
export const useLoginServiceLogoutKey = "LoginServiceLogout";
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 1179186d336..15003eedc51 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -2163,6 +2163,13 @@ export const ensureUseLoginServiceLoginData =
(queryClient: QueryClient, { next
next?: string;
} = {}) => queryClient.ensureQueryData({ queryKey:
Common.UseLoginServiceLoginKeyFn({ next }), queryFn: () => LoginService.login({
next }) });
/**
+* Get Cli Login Metadata
+* Return the login methods the AuthManager configured offers to command-line
clients.
+* @returns CliLoginMetadataResponse Successful Response
+* @throws ApiError
+*/
+export const ensureUseLoginServiceGetCliLoginMetadataData = (queryClient:
QueryClient) => queryClient.ensureQueryData({ queryKey:
Common.UseLoginServiceGetCliLoginMetadataKeyFn(), queryFn: () =>
LoginService.getCliLoginMetadata() });
+/**
* Logout
* Logout the user.
* @returns unknown Successful Response
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index 2f7eac20576..489ea7df9b0 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -2163,6 +2163,13 @@ export const prefetchUseLoginServiceLogin =
(queryClient: QueryClient, { next }:
next?: string;
} = {}) => queryClient.prefetchQuery({ queryKey:
Common.UseLoginServiceLoginKeyFn({ next }), queryFn: () => LoginService.login({
next }) });
/**
+* Get Cli Login Metadata
+* Return the login methods the AuthManager configured offers to command-line
clients.
+* @returns CliLoginMetadataResponse Successful Response
+* @throws ApiError
+*/
+export const prefetchUseLoginServiceGetCliLoginMetadata = (queryClient:
QueryClient) => queryClient.prefetchQuery({ queryKey:
Common.UseLoginServiceGetCliLoginMetadataKeyFn(), queryFn: () =>
LoginService.getCliLoginMetadata() });
+/**
* Logout
* Logout the user.
* @returns unknown Successful Response
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 048d540f134..e3a0c653bd0 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -2163,6 +2163,13 @@ export const useLoginServiceLogin = <TData =
Common.LoginServiceLoginDefaultResp
next?: string;
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseLoginServiceLoginKeyFn({ next }, queryKey), queryFn: () =>
LoginService.login({ next }) as TData, ...options });
/**
+* Get Cli Login Metadata
+* Return the login methods the AuthManager configured offers to command-line
clients.
+* @returns CliLoginMetadataResponse Successful Response
+* @throws ApiError
+*/
+export const useLoginServiceGetCliLoginMetadata = <TData =
Common.LoginServiceGetCliLoginMetadataDefaultResponse, TError = unknown,
TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?:
Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) =>
useQuery<TData, TError>({ queryKey:
Common.UseLoginServiceGetCliLoginMetadataKeyFn(queryKey), queryFn: () =>
LoginService.getCliLoginMetadata() as TData, ...options });
+/**
* Logout
* Logout the user.
* @returns unknown Successful Response
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 71efee38492..368cf359d2a 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -2163,6 +2163,13 @@ export const useLoginServiceLoginSuspense = <TData =
Common.LoginServiceLoginDef
next?: string;
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseLoginServiceLoginKeyFn({ next }, queryKey), queryFn: () =>
LoginService.login({ next }) as TData, ...options });
/**
+* Get Cli Login Metadata
+* Return the login methods the AuthManager configured offers to command-line
clients.
+* @returns CliLoginMetadataResponse Successful Response
+* @throws ApiError
+*/
+export const useLoginServiceGetCliLoginMetadataSuspense = <TData =
Common.LoginServiceGetCliLoginMetadataDefaultResponse, TError = unknown,
TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?:
Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) =>
useSuspenseQuery<TData, TError>({ queryKey:
Common.UseLoginServiceGetCliLoginMetadataKeyFn(queryKey), queryFn: () =>
LoginService.getCliLoginMetadata() as TData, ...options });
+/**
* Logout
* Logout the user.
* @returns unknown Successful Response
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 47cb8ea9bdf..60ac0c48430 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -2577,6 +2577,62 @@ export const $ClearTaskInstancesBody = {
description: 'Request body for Clear Task Instances endpoint.'
} as const;
+export const $CliLoginMetadataResponse = {
+ properties: {
+ grant_types_supported: {
+ items: {
+ type: 'string'
+ },
+ type: 'array',
+ title: 'Grant Types Supported'
+ },
+ code_challenge_methods_supported: {
+ items: {
+ type: 'string'
+ },
+ type: 'array',
+ title: 'Code Challenge Methods Supported'
+ },
+ authorization_endpoint: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Authorization Endpoint'
+ },
+ device_authorization_endpoint: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Device Authorization Endpoint'
+ },
+ token_endpoint: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Token Endpoint'
+ }
+ },
+ type: 'object',
+ required: ['grant_types_supported', 'code_challenge_methods_supported',
'authorization_endpoint', 'device_authorization_endpoint', 'token_endpoint'],
+ title: 'CliLoginMetadataResponse',
+ description: 'Login methods the auth manager offers to command-line
clients, named as in RFC 8414.'
+} as const;
+
export const $Config = {
properties: {
sections: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index ee4130f8e15..79964c5104c 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
-import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData,
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse,
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData,
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse,
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse,
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData,
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse,
Dele [...]
+import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData,
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse,
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData,
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse,
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse,
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData,
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse,
Dele [...]
export class AssetService {
/**
@@ -5305,6 +5305,19 @@ export class LoginService {
});
}
+ /**
+ * Get Cli Login Metadata
+ * Return the login methods the AuthManager configured offers to
command-line clients.
+ * @returns CliLoginMetadataResponse Successful Response
+ * @throws ApiError
+ */
+ public static getCliLoginMetadata():
CancelablePromise<GetCliLoginMetadataResponse> {
+ return __request(OpenAPI, {
+ method: 'GET',
+ url: '/api/v2/auth/cli/metadata'
+ });
+ }
+
/**
* Logout
* Logout the user.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index fe19ce55c37..eea8bebd212 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -800,6 +800,17 @@ export type ClearTaskInstancesBody = {
note?: string | null;
};
+/**
+ * Login methods the auth manager offers to command-line clients, named as in
RFC 8414.
+ */
+export type CliLoginMetadataResponse = {
+ grant_types_supported: Array<(string)>;
+ code_challenge_methods_supported: Array<(string)>;
+ authorization_endpoint: string | null;
+ device_authorization_endpoint: string | null;
+ token_endpoint: string | null;
+};
+
/**
* List of config sections with their options.
*/
@@ -5637,6 +5648,8 @@ export type LoginData = {
export type LoginResponse = unknown;
+export type GetCliLoginMetadataResponse = CliLoginMetadataResponse;
+
export type LogoutResponse = unknown;
export type GetAuthMenusResponse = MenuItemCollectionResponse;
@@ -9949,6 +9962,16 @@ export type $OpenApiTs = {
};
};
};
+ '/api/v2/auth/cli/metadata': {
+ get: {
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: CliLoginMetadataResponse;
+ };
+ };
+ };
'/api/v2/auth/logout': {
get: {
res: {
diff --git
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
index e433c1223da..f0de3a0b4e5 100644
---
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
+++
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
@@ -38,7 +38,7 @@ from
airflow.api_fastapi.auth.managers.models.resource_details import (
)
from airflow.api_fastapi.auth.middlewares.refresh_token import
JWTRefreshMiddleware
from airflow.api_fastapi.auth.tokens import JWTGenerator, JWTValidator
-from airflow.api_fastapi.common.types import MenuItem
+from airflow.api_fastapi.common.types import CliLoginMethods, MenuItem
from airflow.api_fastapi.core_api.app import _STATIC_URL_PATH
from airflow.exceptions import RemovedInAirflow4Warning
from airflow.models.pool import Pool
@@ -274,6 +274,9 @@ class TestBaseAuthManager:
def test_get_url_logout_return_none(self, auth_manager):
assert auth_manager.get_url_logout() is None
+ def test_get_cli_login_methods_return_empty(self, auth_manager):
+ assert auth_manager.get_cli_login_methods() == CliLoginMethods()
+
def test_get_extra_menu_items_return_empty_list(self, auth_manager):
assert
auth_manager.get_extra_menu_items(user=BaseAuthManagerUserTest(name="test")) ==
[]
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py
index 6fd0143388f..3b9732ff476 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py
@@ -24,7 +24,8 @@ import jwt
import pytest
from fastapi.testclient import TestClient
-from airflow.api_fastapi.auth.managers.base_auth_manager import
COOKIE_NAME_JWT_TOKEN
+from airflow.api_fastapi.auth.managers.base_auth_manager import
COOKIE_NAME_JWT_TOKEN, BaseAuthManager
+from airflow.api_fastapi.common.types import CliLoginMethods
from airflow.models.revoked_token import RevokedToken
from tests_common.test_utils.config import conf_vars
@@ -98,6 +99,50 @@ class TestGetLogin(TestAuthEndpoint):
assert response.status_code == 400
+class TestGetCliLoginMetadata:
+ @pytest.mark.parametrize(
+ ("methods", "expected"),
+ [
+ pytest.param(
+ CliLoginMethods(),
+ {
+ "grant_types_supported": [],
+ "code_challenge_methods_supported": [],
+ "authorization_endpoint": None,
+ "device_authorization_endpoint": None,
+ "token_endpoint": None,
+ },
+ id="no-method",
+ ),
+ pytest.param(
+ CliLoginMethods(
+ grant_types_supported=["password", "authorization_code"],
+ code_challenge_methods_supported=["S256"],
+
authorization_endpoint="http://localhost:8080/auth/login/cli",
+ token_endpoint="http://localhost:8080/auth/token/cli",
+ ),
+ {
+ "grant_types_supported": ["password",
"authorization_code"],
+ "code_challenge_methods_supported": ["S256"],
+ "authorization_endpoint":
"http://localhost:8080/auth/login/cli",
+ "device_authorization_endpoint": None,
+ "token_endpoint": "http://localhost:8080/auth/token/cli",
+ },
+ id="authorization-code",
+ ),
+ ],
+ )
+ def test_should_respond_200_without_authentication(self,
unauthenticated_test_client, methods, expected):
+ auth_manager_mock = MagicMock(spec=BaseAuthManager)
+ auth_manager_mock.get_cli_login_methods.return_value = methods
+ unauthenticated_test_client.app.state.auth_manager = auth_manager_mock
+
+ response = unauthenticated_test_client.get("/auth/cli/metadata")
+
+ assert response.status_code == 200
+ assert response.json() == expected
+
+
class TestLogout(TestAuthEndpoint):
@pytest.fixture(autouse=True)
def cleanup_revoked_tokens(self):
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py
index 89d4ed29bae..f8767fff0b3 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py
@@ -27,6 +27,7 @@ from tests_common.test_utils.api_fastapi import
iter_api_routes
NO_AUTH_PATHS = {
"/api/v2/auth/login",
"/api/v2/auth/logout",
+ "/api/v2/auth/cli/metadata",
"/api/v2/version",
"/api/v2/monitor/health",
}
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index fb299db1a10..77590a0ceb3 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -346,6 +346,18 @@ class ClearTaskInstancesBody(BaseModel):
note: Annotated[Note | None, Field(title="Note")] = None
+class CliLoginMetadataResponse(BaseModel):
+ """
+ Login methods the auth manager offers to command-line clients, named as in
RFC 8414.
+ """
+
+ grant_types_supported: Annotated[list[str], Field(title="Grant Types
Supported")]
+ code_challenge_methods_supported: Annotated[list[str], Field(title="Code
Challenge Methods Supported")]
+ authorization_endpoint: Annotated[str | None, Field(title="Authorization
Endpoint")]
+ device_authorization_endpoint: Annotated[str | None, Field(title="Device
Authorization Endpoint")]
+ token_endpoint: Annotated[str | None, Field(title="Token Endpoint")]
+
+
class Value(RootModel[tuple[str, str]]):
root: Annotated[tuple[str, str], Field(max_length=2, min_length=2,
title="Value")]
diff --git a/providers/fab/docs/auth-manager/access-control.rst
b/providers/fab/docs/auth-manager/access-control.rst
index 58554ae78af..0d203cdaffe 100644
--- a/providers/fab/docs/auth-manager/access-control.rst
+++ b/providers/fab/docs/auth-manager/access-control.rst
@@ -284,6 +284,10 @@ Stable API Permissions
- PUT
- Assets.can_edit
- Op
+ * - ``/api/v2/auth/cli/metadata``
+ - GET
+ - None
+ - Public
* - ``/api/v2/auth/login``
- GET
- None