
Add 'user_default_domain' config option to replace the hardcoded "Default" domain when users login without specifying a domain. Also add '/config' API endpoint to expose public configurations before authentication. Currently returns user_default_domain and can be extended for other public configs. Changes: - Add user_default_domain in openstack config section - Add GET /config endpoint (no auth required) - Use generic Config schema for extensibility - Update docs and sample files This improves flexibility for multi-domain deployments. Change-Id: I6fc40337c9835cf8172bcfa86cac621b7effeb0d Signed-off-by: Reet Srivastava <reet.srivastava@rackspace.com> Signed-off-by: Wu Wenxiang <wu.wenxiang@algoblu.com>
163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
# Copyright 2021 99cloud
|
|
#
|
|
# Licensed 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
|
|
|
|
import time
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from typing import AsyncGenerator
|
|
|
|
import jose
|
|
from fastapi import FastAPI, Request, status
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
|
|
from skyline_apiserver.api.v1 import api_router
|
|
from skyline_apiserver.config import CONF, configure
|
|
from skyline_apiserver.context import RequestContext
|
|
from skyline_apiserver.core.security import generate_profile_by_token, parse_access_token
|
|
from skyline_apiserver.db import api as db_api, setup as db_setup
|
|
from skyline_apiserver.log import LOG, setup as log_setup
|
|
from skyline_apiserver.policy import setup as policies_setup
|
|
from skyline_apiserver.types import constants
|
|
|
|
PROJECT_NAME = "Skyline API"
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
configure("skyline")
|
|
log_setup(
|
|
Path(CONF.default.log_dir).joinpath(CONF.default.log_file),
|
|
debug=CONF.default.debug,
|
|
)
|
|
policies_setup()
|
|
db_setup()
|
|
|
|
# Set all CORS enabled origins
|
|
if CONF.default.cors_allow_origins:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[str(origin) for origin in CONF.default.cors_allow_origins],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
LOG.debug("Skyline API server start")
|
|
yield
|
|
LOG.debug("Skyline API server stop")
|
|
|
|
|
|
app = FastAPI(
|
|
title=PROJECT_NAME,
|
|
openapi_url=f"{constants.API_PREFIX}/openapi.json",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def validate_token(request: Request, call_next):
|
|
url_path = request.url.path
|
|
LOG.debug(f"Request path: {url_path}")
|
|
|
|
# Skip authentication for login and static endpoints
|
|
ignore_urls = [
|
|
f"{constants.API_PREFIX}/login",
|
|
f"{constants.API_PREFIX}/websso",
|
|
"/static",
|
|
"/docs",
|
|
f"{constants.API_PREFIX}/openapi.json",
|
|
"/favicon.ico",
|
|
f"{constants.API_PREFIX}/sso",
|
|
f"{constants.API_PREFIX}/config",
|
|
f"{constants.API_PREFIX}/contrib/keystone_endpoints",
|
|
# f"{constants.API_PREFIX}/contrib/domains",
|
|
f"{constants.API_PREFIX}/contrib/regions",
|
|
]
|
|
|
|
for ignore_url in ignore_urls:
|
|
if url_path.startswith(ignore_url):
|
|
return await call_next(request)
|
|
|
|
if url_path.startswith(constants.API_PREFIX):
|
|
# Get token from cookie
|
|
token = request.cookies.get(CONF.default.session_name)
|
|
if not token:
|
|
return JSONResponse(
|
|
content={"message": "Unauthorized: Token not found"},
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
|
|
try:
|
|
# Purge revoked tokens
|
|
db_api.purge_revoked_token()
|
|
|
|
# Parse and validate token
|
|
parsed_token = parse_access_token(token)
|
|
is_revoked = db_api.check_token(parsed_token.uuid)
|
|
if is_revoked:
|
|
return JSONResponse(
|
|
content={"message": "Unauthorized: Token revoked"},
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
|
|
# Generate profile from token
|
|
profile = generate_profile_by_token(parsed_token)
|
|
|
|
# Create RequestContext from profile
|
|
request.state.context = RequestContext(
|
|
user_id=profile.user.id,
|
|
project_id=profile.project.id,
|
|
project_name=profile.project.name,
|
|
user_domain_id=profile.user.domain.id,
|
|
project_domain_id=profile.project.domain.id,
|
|
roles=[role.name for role in profile.roles],
|
|
auth_token=profile.keystone_token,
|
|
)
|
|
|
|
# Store profile in request state for backward compatibility
|
|
request.state.profile = profile
|
|
|
|
# Check if token needs renewal
|
|
if 0 < profile.exp - time.time() < CONF.default.access_token_renew:
|
|
profile.exp = int(time.time()) + CONF.default.access_token_expire
|
|
# Note: We can't set cookies in middleware, so we'll handle this in the response
|
|
request.state.token_needs_renewal = True
|
|
request.state.new_token = profile.toJWTPayload()
|
|
request.state.new_exp = str(profile.exp)
|
|
|
|
except jose.exceptions.ExpiredSignatureError as e:
|
|
return JSONResponse(
|
|
content={"message": f"Unauthorized: Token expired - {str(e)}"},
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
content={"message": f"Unauthorized: {str(e)}"},
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
)
|
|
|
|
response = await call_next(request)
|
|
|
|
# Handle token renewal in response
|
|
if hasattr(request.state, "token_needs_renewal") and request.state.token_needs_renewal:
|
|
response.set_cookie(CONF.default.session_name, request.state.new_token)
|
|
response.set_cookie(constants.TIME_EXPIRED_KEY, request.state.new_exp)
|
|
|
|
return response
|
|
|
|
|
|
app.include_router(api_router, prefix=constants.API_PREFIX)
|