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": {
|
"/api/v1/profile": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"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": {
|
"ComputeServicesResponse": {
|
||||||
"title": "ComputeServicesResponse",
|
"title": "ComputeServicesResponse",
|
||||||
"required": [
|
"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": {
|
"ServerSortKey": {
|
||||||
"title": "ServerSortKey",
|
"title": "ServerSortKey",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -11,6 +11,7 @@ default:
|
|||||||
prometheus_endpoint: http://localhost:9091
|
prometheus_endpoint: http://localhost:9091
|
||||||
secret_key: aCtmgbcUqYUy_HNVg5BDXCaeJgJQzHJXwqbXr0Nmb2o
|
secret_key: aCtmgbcUqYUy_HNVg5BDXCaeJgJQzHJXwqbXr0Nmb2o
|
||||||
session_name: session
|
session_name: session
|
||||||
|
ssl_enabled: true
|
||||||
openstack:
|
openstack:
|
||||||
base_domains:
|
base_domains:
|
||||||
- heat_user_domain
|
- heat_user_domain
|
||||||
@ -40,6 +41,10 @@ openstack:
|
|||||||
placement: placement
|
placement: placement
|
||||||
sharev2: manilav2
|
sharev2: manilav2
|
||||||
volumev3: cinder
|
volumev3: cinder
|
||||||
|
sso_enabled: false
|
||||||
|
sso_protocols:
|
||||||
|
- openid
|
||||||
|
sso_region: RegionOne
|
||||||
system_admin_roles:
|
system_admin_roles:
|
||||||
- admin
|
- admin
|
||||||
- system_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 __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response, status
|
from pathlib import PurePath
|
||||||
from keystoneauth1.identity.v3 import Password
|
|
||||||
|
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 keystoneauth1.session import Session
|
||||||
from keystoneclient.client import Client as KeystoneClient
|
from keystoneclient.client import Client as KeystoneClient
|
||||||
|
|
||||||
@ -126,6 +129,118 @@ async def login(
|
|||||||
return profile
|
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(
|
@router.get(
|
||||||
"/profile",
|
"/profile",
|
||||||
description="Get user profile.",
|
description="Get user profile.",
|
||||||
|
@ -104,6 +104,13 @@ prometheus_basic_auth_password = Opt(
|
|||||||
default="",
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ssl_enabled = Opt(
|
||||||
|
name="ssl_enabled",
|
||||||
|
description="enable ssl",
|
||||||
|
schema=StrictBool,
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
GROUP_NAME = __name__.split(".")[-1]
|
GROUP_NAME = __name__.split(".")[-1]
|
||||||
ALL_OPTS = (
|
ALL_OPTS = (
|
||||||
debug,
|
debug,
|
||||||
@ -113,6 +120,7 @@ ALL_OPTS = (
|
|||||||
access_token_renew,
|
access_token_renew,
|
||||||
cors_allow_origins,
|
cors_allow_origins,
|
||||||
session_name,
|
session_name,
|
||||||
|
ssl_enabled,
|
||||||
database_url,
|
database_url,
|
||||||
prometheus_endpoint,
|
prometheus_endpoint,
|
||||||
prometheus_enable_basic_auth,
|
prometheus_enable_basic_auth,
|
||||||
|
@ -16,7 +16,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Dict, List
|
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.config.base import Opt
|
||||||
from skyline_apiserver.types import InterfaceType
|
from skyline_apiserver.types import InterfaceType
|
||||||
@ -152,9 +152,34 @@ reclaim_instance_interval = Opt(
|
|||||||
default=60 * 60 * 24 * 7,
|
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]
|
GROUP_NAME = __name__.split(".")[-1]
|
||||||
ALL_OPTS = (
|
ALL_OPTS = (
|
||||||
|
sso_enabled,
|
||||||
|
sso_protocols,
|
||||||
|
sso_region,
|
||||||
keystone_url,
|
keystone_url,
|
||||||
system_project_domain,
|
system_project_domain,
|
||||||
system_project,
|
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.db import setup as db_setup
|
||||||
from skyline_apiserver.log import LOG, setup as log_setup
|
from skyline_apiserver.log import LOG, setup as log_setup
|
||||||
from skyline_apiserver.policy import setup as policies_setup
|
from skyline_apiserver.policy import setup as policies_setup
|
||||||
|
from skyline_apiserver.types import constants
|
||||||
|
|
||||||
PROJECT_NAME = "Skyline API"
|
PROJECT_NAME = "Skyline API"
|
||||||
API_PREFIX = "/api/v1"
|
|
||||||
|
|
||||||
|
|
||||||
async def on_startup() -> None:
|
async def on_startup() -> None:
|
||||||
@ -56,9 +56,9 @@ async def on_shutdown() -> None:
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=PROJECT_NAME,
|
title=PROJECT_NAME,
|
||||||
openapi_url=f"{API_PREFIX}/openapi.json",
|
openapi_url=f"{constants.API_PREFIX}/openapi.json",
|
||||||
on_startup=[on_startup],
|
on_startup=[on_startup],
|
||||||
on_shutdown=[on_shutdown],
|
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,
|
VolumesResponse,
|
||||||
VolumeStatus,
|
VolumeStatus,
|
||||||
)
|
)
|
||||||
from .login import Credential, Payload, Profile
|
from .login import SSO, Credential, Payload, Profile
|
||||||
from .policy import Policies, PoliciesRules
|
from .policy import Policies, PoliciesRules
|
||||||
from .policy_manager import Operation, OperationsSchema, ScopeTypesSchema
|
from .policy_manager import Operation, OperationsSchema, ScopeTypesSchema
|
||||||
from .prometheus import (
|
from .prometheus import (
|
||||||
|
@ -106,3 +106,13 @@ class Profile(PayloadBase):
|
|||||||
|
|
||||||
def toJWTPayload(self) -> str:
|
def toJWTPayload(self) -> str:
|
||||||
return self.toPayLoad().toJWTPayload()
|
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"
|
POLICY_NS = "oslo.policy.policies"
|
||||||
|
|
||||||
|
API_PREFIX = "/api/v1"
|
||||||
|
|
||||||
SUPPORTED_SERVICE_EPS = {
|
SUPPORTED_SERVICE_EPS = {
|
||||||
# openstack_service: [<entry_point_name>, <entry_point_name>,]
|
# openstack_service: [<entry_point_name>, <entry_point_name>,]
|
||||||
"barbican": ["barbican"],
|
"barbican": ["barbican"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user