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:
yangshaoxue 2022-08-08 19:02:24 +08:00 committed by yangsngshaoxue
parent f6cf14786b
commit cbabcbce89
10 changed files with 308 additions and 7 deletions

View File

@ -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": [

View File

@ -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

View File

@ -0,0 +1,4 @@
---
features:
- |
Add single sign-on (SSO) support. Skyline login with SSO configured with OpenID Connect.

View File

@ -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.",

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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 (

View File

@ -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]

View File

@ -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"],