feat: Support SSO login via openID
1. add API sso and websso 2. add sso conf Implements: blueprint skyline-sso-oid Change-Id: I352200bb2ebf426adaea71826253730c51eeee03
This commit is contained in:
parent
f6cf14786b
commit
cbabcbce89
@ -70,6 +70,86 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/sso": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Login"
|
||||
],
|
||||
"summary": "Get Sso",
|
||||
"description": "SSO configuration.",
|
||||
"operationId": "get_sso_api_v1_sso_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SSO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/websso": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Login"
|
||||
],
|
||||
"summary": "Websso",
|
||||
"description": "Websso",
|
||||
"operationId": "websso_api_v1_websso_post",
|
||||
"parameters": [
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"title": "X-Openstack-Request-Id",
|
||||
"pattern": "^req-\\w{8}(-\\w{4}){3}-\\w{12}",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"name": "X-Openstack-Request-Id",
|
||||
"in": "header"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_websso_api_v1_websso_post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Redirect"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedMessage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/profile": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -1903,6 +1983,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Body_websso_api_v1_websso_post": {
|
||||
"title": "Body_websso_api_v1_websso_post",
|
||||
"required": [
|
||||
"token"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"title": "Token",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComputeServicesResponse": {
|
||||
"title": "ComputeServicesResponse",
|
||||
"required": [
|
||||
@ -2871,6 +2964,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SSO": {
|
||||
"title": "SSO",
|
||||
"required": [
|
||||
"enable_sso",
|
||||
"protocols"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enable_sso": {
|
||||
"title": "Enable Sso",
|
||||
"type": "boolean"
|
||||
},
|
||||
"protocols": {
|
||||
"title": "Protocols",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SSOInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SSOInfo": {
|
||||
"title": "SSOInfo",
|
||||
"required": [
|
||||
"protocol",
|
||||
"url"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"protocol": {
|
||||
"title": "Protocol",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"title": "Url",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerSortKey": {
|
||||
"title": "ServerSortKey",
|
||||
"enum": [
|
||||
|
@ -11,6 +11,7 @@ default:
|
||||
prometheus_endpoint: http://localhost:9091
|
||||
secret_key: aCtmgbcUqYUy_HNVg5BDXCaeJgJQzHJXwqbXr0Nmb2o
|
||||
session_name: session
|
||||
ssl_enabled: true
|
||||
openstack:
|
||||
base_domains:
|
||||
- heat_user_domain
|
||||
@ -40,6 +41,10 @@ openstack:
|
||||
placement: placement
|
||||
sharev2: manilav2
|
||||
volumev3: cinder
|
||||
sso_enabled: false
|
||||
sso_protocols:
|
||||
- openid
|
||||
sso_region: RegionOne
|
||||
system_admin_roles:
|
||||
- admin
|
||||
- system_admin
|
||||
|
4
releasenotes/notes/add-sso-login-via-openid.yaml
Normal file
4
releasenotes/notes/add-sso-login-via-openid.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add single sign-on (SSO) support. Skyline login with SSO configured with OpenID Connect.
|
@ -14,8 +14,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response, status
|
||||
from keystoneauth1.identity.v3 import Password
|
||||
from pathlib import PurePath
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from keystoneauth1.identity.v3 import Password, Token
|
||||
from keystoneauth1.session import Session
|
||||
from keystoneclient.client import Client as KeystoneClient
|
||||
|
||||
@ -126,6 +129,118 @@ async def login(
|
||||
return profile
|
||||
|
||||
|
||||
@router.get(
|
||||
"/sso",
|
||||
description="SSO configuration.",
|
||||
responses={
|
||||
200: {"model": schemas.SSO},
|
||||
},
|
||||
response_model=schemas.SSO,
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_description="OK",
|
||||
)
|
||||
async def get_sso(
|
||||
request: Request,
|
||||
) -> schemas.SSO:
|
||||
sso = {
|
||||
"enable_sso": False,
|
||||
"protocols": [],
|
||||
}
|
||||
if CONF.openstack.sso_enabled:
|
||||
protocols = []
|
||||
|
||||
ks_url = CONF.openstack.keystone_url.rstrip("/")
|
||||
url_scheme = "https" if CONF.default.ssl_enabled else "http"
|
||||
base_url = f"{url_scheme}://{request.url.hostname}:{request.url.port}"
|
||||
base_path = str(PurePath("/").joinpath(CONF.openstack.nginx_prefix, "skyline"))
|
||||
|
||||
for protocol in CONF.openstack.sso_protocols:
|
||||
|
||||
url = (
|
||||
f"{ks_url}/auth/OS-FEDERATION/websso/{protocol}"
|
||||
f"?origin={base_url}{base_path}{constants.API_PREFIX}/websso"
|
||||
)
|
||||
|
||||
protocols.append(
|
||||
{
|
||||
"protocol": protocol,
|
||||
"url": url,
|
||||
}
|
||||
)
|
||||
|
||||
sso = {
|
||||
"enable_sso": CONF.openstack.sso_enabled,
|
||||
"protocols": protocols,
|
||||
}
|
||||
|
||||
return schemas.SSO(**sso)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/websso",
|
||||
description="Websso",
|
||||
responses={
|
||||
302: {"class": RedirectResponse},
|
||||
401: {"model": schemas.common.UnauthorizedMessage},
|
||||
},
|
||||
response_class=RedirectResponse,
|
||||
status_code=status.HTTP_302_FOUND,
|
||||
response_description="Redirect",
|
||||
)
|
||||
async def websso(
|
||||
token: str = Form(...),
|
||||
x_openstack_request_id: str = Header(
|
||||
"",
|
||||
alias=constants.INBOUND_HEADER,
|
||||
regex=constants.INBOUND_HEADER_REGEX,
|
||||
),
|
||||
) -> RedirectResponse:
|
||||
try:
|
||||
auth_url = await utils.get_endpoint(
|
||||
region=CONF.openstack.sso_region,
|
||||
service="keystone",
|
||||
session=get_system_session(),
|
||||
)
|
||||
unscope_auth = Token(
|
||||
auth_url=auth_url,
|
||||
token=token,
|
||||
reauthenticate=False,
|
||||
)
|
||||
session = Session(auth=unscope_auth, verify=False, timeout=constants.DEFAULT_TIMEOUT)
|
||||
unscope_client = KeystoneClient(
|
||||
session=session,
|
||||
endpoint=auth_url,
|
||||
interface=CONF.openstack.interface_type,
|
||||
)
|
||||
project_scope = unscope_client.auth.projects()
|
||||
# we must get the project_scope with enabled project
|
||||
project_scope = [scope for scope in project_scope if scope.enabled]
|
||||
if not project_scope:
|
||||
raise Exception("You are not authorized for any projects or domains.")
|
||||
|
||||
project_scope_token = await get_project_scope_token(
|
||||
keystone_token=token,
|
||||
region=CONF.openstack.sso_region,
|
||||
project_id=project_scope[0].id,
|
||||
)
|
||||
|
||||
profile = await generate_profile(
|
||||
keystone_token=project_scope_token,
|
||||
region=CONF.openstack.sso_region,
|
||||
)
|
||||
|
||||
profile = await _patch_profile(profile, x_openstack_request_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(e),
|
||||
)
|
||||
else:
|
||||
response = RedirectResponse(url="/base/overview", status_code=status.HTTP_302_FOUND)
|
||||
response.set_cookie(CONF.default.session_name, profile.toJWTPayload())
|
||||
return response
|
||||
|
||||
|
||||
@router.get(
|
||||
"/profile",
|
||||
description="Get user profile.",
|
||||
|
@ -104,6 +104,13 @@ prometheus_basic_auth_password = Opt(
|
||||
default="",
|
||||
)
|
||||
|
||||
ssl_enabled = Opt(
|
||||
name="ssl_enabled",
|
||||
description="enable ssl",
|
||||
schema=StrictBool,
|
||||
default=True,
|
||||
)
|
||||
|
||||
GROUP_NAME = __name__.split(".")[-1]
|
||||
ALL_OPTS = (
|
||||
debug,
|
||||
@ -113,6 +120,7 @@ ALL_OPTS = (
|
||||
access_token_renew,
|
||||
cors_allow_origins,
|
||||
session_name,
|
||||
ssl_enabled,
|
||||
database_url,
|
||||
prometheus_endpoint,
|
||||
prometheus_enable_basic_auth,
|
||||
|
@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from pydantic import HttpUrl, StrictInt, StrictStr
|
||||
from pydantic import HttpUrl, StrictBool, StrictInt, StrictStr
|
||||
|
||||
from skyline_apiserver.config.base import Opt
|
||||
from skyline_apiserver.types import InterfaceType
|
||||
@ -152,9 +152,34 @@ reclaim_instance_interval = Opt(
|
||||
default=60 * 60 * 24 * 7,
|
||||
)
|
||||
|
||||
sso_enabled = Opt(
|
||||
name="sso_enabled",
|
||||
description="enable sso",
|
||||
schema=StrictBool,
|
||||
default=False,
|
||||
)
|
||||
|
||||
sso_protocols = Opt(
|
||||
name="sso_protocols",
|
||||
description="SSO protocol list",
|
||||
schema=List[StrictStr],
|
||||
default=[
|
||||
"openid",
|
||||
],
|
||||
)
|
||||
|
||||
sso_region = Opt(
|
||||
name="sso_region",
|
||||
description="SSO region",
|
||||
schema=StrictStr,
|
||||
default="RegionOne",
|
||||
)
|
||||
|
||||
GROUP_NAME = __name__.split(".")[-1]
|
||||
ALL_OPTS = (
|
||||
sso_enabled,
|
||||
sso_protocols,
|
||||
sso_region,
|
||||
keystone_url,
|
||||
system_project_domain,
|
||||
system_project,
|
||||
|
@ -24,9 +24,9 @@ from skyline_apiserver.config import CONF, configure
|
||||
from skyline_apiserver.db import 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"
|
||||
API_PREFIX = "/api/v1"
|
||||
|
||||
|
||||
async def on_startup() -> None:
|
||||
@ -56,9 +56,9 @@ async def on_shutdown() -> None:
|
||||
|
||||
app = FastAPI(
|
||||
title=PROJECT_NAME,
|
||||
openapi_url=f"{API_PREFIX}/openapi.json",
|
||||
openapi_url=f"{constants.API_PREFIX}/openapi.json",
|
||||
on_startup=[on_startup],
|
||||
on_shutdown=[on_shutdown],
|
||||
)
|
||||
|
||||
app.include_router(api_router, prefix=API_PREFIX)
|
||||
app.include_router(api_router, prefix=constants.API_PREFIX)
|
||||
|
@ -42,7 +42,7 @@ from .extension import (
|
||||
VolumesResponse,
|
||||
VolumeStatus,
|
||||
)
|
||||
from .login import Credential, Payload, Profile
|
||||
from .login import SSO, Credential, Payload, Profile
|
||||
from .policy import Policies, PoliciesRules
|
||||
from .policy_manager import Operation, OperationsSchema, ScopeTypesSchema
|
||||
from .prometheus import (
|
||||
|
@ -106,3 +106,13 @@ class Profile(PayloadBase):
|
||||
|
||||
def toJWTPayload(self) -> str:
|
||||
return self.toPayLoad().toJWTPayload()
|
||||
|
||||
|
||||
class SSOInfo(BaseModel):
|
||||
protocol: str
|
||||
url: str
|
||||
|
||||
|
||||
class SSO(BaseModel):
|
||||
enable_sso: bool
|
||||
protocols: List[SSOInfo]
|
||||
|
@ -45,6 +45,8 @@ DEFAULT_TIMEOUT = 30
|
||||
|
||||
POLICY_NS = "oslo.policy.policies"
|
||||
|
||||
API_PREFIX = "/api/v1"
|
||||
|
||||
SUPPORTED_SERVICE_EPS = {
|
||||
# openstack_service: [<entry_point_name>, <entry_point_name>,]
|
||||
"barbican": ["barbican"],
|
||||
|
Loading…
Reference in New Issue
Block a user