blob: 98190f251903f6b7d49e99345c2a9e5270025a6d [file]
# 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 openserverless import app
from openserverless.impl.auth.auth_service import AuthService
from openserverless.impl.auth.oidc_device_flow_service import OidcDeviceFlowService
from openserverless.security.ow_authorize import ow_authorize
from flask import request
import openserverless.common.response_builder as res_builder
from flasgger import swag_from
from urllib.parse import urlparse
def _extract_bearer_token():
authorization = request.headers.get("Authorization", "")
if authorization.lower().startswith("bearer "):
return authorization.split(" ", 1)[1].strip()
body = request.get_json(silent=True) or {}
return body.get("access_token")
def _requested_namespace_from_origin(origin, api_host):
if not origin:
return None
origin_host = urlparse(origin).hostname
if not origin_host:
return None
api_hostname = (api_host or "").split(":", 1)[0]
suffix = f".{api_hostname}"
if origin_host == api_hostname or not origin_host.endswith(suffix):
return None
candidate = origin_host[: -len(suffix)]
if "." in candidate or not candidate:
return None
return candidate
@app.route('/system/api/v1/auth/<login>',methods=['PATCH'])
@ow_authorize(pass_user_data=True)
def password(login, **kwargs):
"""
Update User Password
---
tags:
- Authentication Api
summary: Update user password
description: Update the user password by patching the corresponding WhiskUser entry
operationId: updateUserPassword
security:
- openwhiskBasicAuth: []
consumes:
- application/json
parameters:
- in: path
name: login
description: The username requiring the password update
required: true
type: string
- in: body
name: PasswordUpdate
description: Password update payload containing current and new password
required: true
schema:
$ref: '#/definitions/LoginUpdateData'
responses:
200:
description: Password updated successfully
schema:
$ref: '#/definitions/MessageData'
400:
description: Bad request. Missing required fields.
schema:
$ref: '#/definitions/Message'
401:
description: Unauthorized. Invalid credentials or authorization token.
schema:
$ref: '#/definitions/Message'
"""
update_data = request.get_json()
if 'ow-auth-user' in kwargs:
authorized_data = kwargs['ow-auth-user']
if login not in authorized_data['login']:
return res_builder.build_error_message(f"invalid AUTH token for user {login}", 401)
auth_service = AuthService()
return auth_service.update_password(login,update_data['password'],update_data['new_password'])
@app.route('/system/api/v1/auth',methods=['POST'])
def login():
"""
User Authentication
---
tags:
- Authentication Api
summary: Authenticate user with login credentials
description: Perform user authentication using credentials stored in CouchDB metadata
operationId: authenticateUser
consumes:
- application/json
parameters:
- in: body
name: LoginCredentials
description: User login credentials
required: true
schema:
$ref: '#/definitions/LoginData'
responses:
200:
description: Authentication successful. Returns user data including environment variables and quota.
schema:
$ref: '#/definitions/MessageData'
400:
description: Bad request. Missing login or password.
schema:
$ref: '#/definitions/Message'
401:
description: Unauthorized. Invalid credentials.
schema:
$ref: '#/definitions/Message'
"""
login_data = request.get_json()
auth_service = AuthService()
return auth_service.login(login_data['login'], login_data['password'])
@app.route('/system/api/v1/auth/oidc',methods=['POST'])
def login_oidc():
"""
User Authentication with OIDC
---
tags:
- Authentication Api
summary: Authenticate user with an OIDC access token
description: Validate a Keycloak OIDC token and map it to an existing OpenServerless namespace.
operationId: authenticateUserOidc
consumes:
- application/json
parameters:
- in: header
name: Authorization
description: Bearer token issued by the configured OIDC provider
required: false
type: string
- in: body
name: OidcCredentials
description: Optional JSON payload containing access_token when Authorization header is not used
required: false
schema:
type: object
properties:
access_token:
type: string
responses:
200:
description: Authentication successful. Returns OpenServerless user data.
schema:
$ref: '#/definitions/MessageData'
401:
description: Unauthorized. Invalid or missing OIDC token.
schema:
$ref: '#/definitions/Message'
403:
description: Forbidden. Token is valid but required group is missing.
schema:
$ref: '#/definitions/Message'
404:
description: OIDC user is valid but no OpenServerless namespace is provisioned.
schema:
$ref: '#/definitions/Message'
"""
auth_service = AuthService()
return auth_service.login_oidc(_extract_bearer_token())
@app.route('/system/api/v1/auth/oidc/device/start', methods=['POST'])
def start_oidc_device_login():
"""
Start backend-managed OIDC Device Authorization login
---
tags:
- Authentication Api
summary: Start an SSO login flow managed by admin-api
description: admin-api uses cluster SSO configuration to start OIDC device login and returns only an opaque flow id and user verification data.
operationId: startOidcDeviceLogin
responses:
200:
description: SSO device login flow started.
schema:
type: object
502:
description: admin-api could not start login with the configured OIDC provider.
schema:
$ref: '#/definitions/Message'
"""
body = request.get_json(silent=True) or {}
requested_namespace = body.get("namespace") or _requested_namespace_from_origin(
request.headers.get("Origin"),
request.host,
)
return OidcDeviceFlowService().start(requested_namespace=requested_namespace)
@app.route('/system/api/v1/auth/oidc/device/poll', methods=['POST'])
def poll_oidc_device_login():
"""
Poll backend-managed OIDC Device Authorization login
---
tags:
- Authentication Api
summary: Poll an SSO login flow managed by admin-api
description: admin-api exchanges device credentials with the configured OIDC provider, validates the token and returns OpenServerless namespace data. OIDC tokens are not returned to the browser.
operationId: pollOidcDeviceLogin
consumes:
- application/json
parameters:
- in: body
name: OidcDevicePoll
required: true
schema:
type: object
properties:
flow_id:
type: string
responses:
200:
description: Authentication successful. Returns OpenServerless user data.
schema:
$ref: '#/definitions/MessageData'
202:
description: Login is still pending at the identity provider.
schema:
type: object
400:
description: Login flow expired.
schema:
$ref: '#/definitions/Message'
401:
description: SSO login failed.
schema:
$ref: '#/definitions/Message'
404:
description: Unknown login flow or namespace not provisioned.
schema:
$ref: '#/definitions/Message'
"""
body = request.get_json(silent=True) or {}
return OidcDeviceFlowService().poll(body.get("flow_id"))
@app.route('/system/api/v1/auth/oidc/password', methods=['POST'])
def oidc_password_login():
"""
Backend-managed OIDC password grant login
---
tags:
- Authentication Api
summary: Authenticate user with OIDC password grant
description: admin-api exchanges user credentials with the configured OIDC provider, validates the returned token and returns OpenServerless namespace data. User password and OIDC tokens are not returned.
operationId: oidcPasswordLogin
consumes:
- application/json
parameters:
- in: body
name: OidcPasswordLogin
required: true
schema:
type: object
properties:
username:
type: string
password:
type: string
namespace:
type: string
responses:
200:
description: Authentication successful. Returns OpenServerless user data.
schema:
$ref: '#/definitions/MessageData'
400:
description: Missing username or password.
schema:
$ref: '#/definitions/Message'
401:
description: SSO login failed.
schema:
$ref: '#/definitions/Message'
502:
description: admin-api could not authenticate with the configured OIDC provider.
schema:
$ref: '#/definitions/Message'
"""
body = request.get_json(silent=True) or {}
return OidcDeviceFlowService().password(
body.get("username"),
body.get("password"),
requested_namespace=body.get("namespace"),
)