feat: Add default_project_id

1. add default_project_id into profile to return
2. if user has default_project_id, then we will login into
this project as default.

Change-Id: I147f7866163ae4d102e83f7c28bbf0077f463974
This commit is contained in:
Boxiang Zhu 2023-11-17 17:55:16 +08:00
parent 99f557e19c
commit 235f7fb286
5 changed files with 84 additions and 10 deletions

View File

@ -0,0 +1,6 @@
---
features:
- |
Add ``default_project_id`` into profile to return.
- |
If user has default_project_id, then we will login into this project as default.

View File

@ -15,7 +15,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import PurePath from pathlib import PurePath
from typing import Any, List, Optional, Tuple from typing import Any, List, Optional, Tuple, Union
from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, Response, status from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, Response, status
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
@ -26,7 +26,7 @@ from keystoneclient.client import Client as KeystoneClient
from skyline_apiserver import schemas from skyline_apiserver import schemas
from skyline_apiserver.api import deps from skyline_apiserver.api import deps
from skyline_apiserver.client import utils from skyline_apiserver.client import utils
from skyline_apiserver.client.openstack.keystone import revoke_token from skyline_apiserver.client.openstack.keystone import get_token_data, get_user, revoke_token
from skyline_apiserver.client.openstack.system import ( from skyline_apiserver.client.openstack.system import (
get_endpoints, get_endpoints,
get_project_scope_token, get_project_scope_token,
@ -46,6 +46,19 @@ from skyline_apiserver.types import constants
router = APIRouter() router = APIRouter()
async def _get_default_project_id(
session: Session, region: str, user_id: Optional[str] = None
) -> Union[str, None]:
if not user_id:
token = session.get_token()
token_data = await get_token_data(token, region, session)
_user_id = token_data["token"]["user"]["id"]
else:
_user_id = user_id
user = await get_user(_user_id, region, session)
return getattr(user, "default_project_id", None)
async def _get_projects_and_unscope_token( async def _get_projects_and_unscope_token(
region: str, region: str,
domain: Optional[str] = None, domain: Optional[str] = None,
@ -53,7 +66,7 @@ async def _get_projects_and_unscope_token(
password: Optional[str] = None, password: Optional[str] = None,
token: Optional[str] = None, token: Optional[str] = None,
project_enabled: bool = False, project_enabled: bool = False,
) -> Tuple[List[Any], str]: ) -> Tuple[List[Any], str, Union[str, None]]:
auth_url = await utils.get_endpoint( auth_url = await utils.get_endpoint(
region=region, region=region,
service="keystone", service="keystone",
@ -78,6 +91,9 @@ async def _get_projects_and_unscope_token(
session = Session( session = Session(
auth=unscope_auth, verify=CONF.default.cafile, timeout=constants.DEFAULT_TIMEOUT auth=unscope_auth, verify=CONF.default.cafile, timeout=constants.DEFAULT_TIMEOUT
) )
default_project_id = await _get_default_project_id(session, region)
unscope_client = KeystoneClient( unscope_client = KeystoneClient(
session=session, session=session,
endpoint=auth_url, endpoint=auth_url,
@ -93,7 +109,7 @@ async def _get_projects_and_unscope_token(
if not project_scope: if not project_scope:
raise Exception("You are not authorized for any projects or domains.") raise Exception("You are not authorized for any projects or domains.")
return project_scope, unscope_token return project_scope, unscope_token, default_project_id
async def _patch_profile(profile: schemas.Profile, global_request_id: str) -> schemas.Profile: async def _patch_profile(profile: schemas.Profile, global_request_id: str) -> schemas.Profile:
@ -107,9 +123,13 @@ async def _patch_profile(profile: schemas.Profile, global_request_id: str) -> sc
) )
if not projects: if not projects:
projects, _ = await _get_projects_and_unscope_token( projects, _, default_project_id = await _get_projects_and_unscope_token(
region=profile.region, token=profile.keystone_token region=profile.region, token=profile.keystone_token
) )
else:
default_project_id = await _get_default_project_id(
get_system_session(), profile.region, user_id=profile.user.id
)
profile.projects = { profile.projects = {
i.id: { i.id: {
@ -121,6 +141,8 @@ async def _patch_profile(profile: schemas.Profile, global_request_id: str) -> sc
for i in projects for i in projects
} }
profile.default_project_id = default_project_id
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -151,7 +173,7 @@ async def login(
), ),
) -> schemas.Profile: ) -> schemas.Profile:
try: try:
project_scope, unscope_token = await _get_projects_and_unscope_token( project_scope, unscope_token, default_project_id = await _get_projects_and_unscope_token(
region=credential.region, region=credential.region,
domain=credential.domain, domain=credential.domain,
username=credential.username, username=credential.username,
@ -162,7 +184,7 @@ async def login(
project_scope_token = await get_project_scope_token( project_scope_token = await get_project_scope_token(
keystone_token=unscope_token, keystone_token=unscope_token,
region=credential.region, region=credential.region,
project_id=project_scope[0].id, project_id=default_project_id or project_scope[0].id,
) )
profile = await generate_profile( profile = await generate_profile(
@ -248,7 +270,7 @@ async def websso(
), ),
) -> RedirectResponse: ) -> RedirectResponse:
try: try:
project_scope, _ = await _get_projects_and_unscope_token( project_scope, _, default_project_id = await _get_projects_and_unscope_token(
region=CONF.openstack.sso_region, region=CONF.openstack.sso_region,
token=token, token=token,
project_enabled=True, project_enabled=True,
@ -257,7 +279,7 @@ async def websso(
project_scope_token = await get_project_scope_token( project_scope_token = await get_project_scope_token(
keystone_token=token, keystone_token=token,
region=CONF.openstack.sso_region, region=CONF.openstack.sso_region,
project_id=project_scope[0].id, project_id=default_project_id or project_scope[0].id,
) )
profile = await generate_profile( profile = await generate_profile(

View File

@ -17,7 +17,7 @@ from __future__ import annotations
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from fastapi import HTTPException, status from fastapi import HTTPException, status
from keystoneauth1.exceptions.http import Unauthorized from keystoneauth1.exceptions.http import NotFound, Unauthorized
from keystoneauth1.session import Session from keystoneauth1.session import Session
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
@ -82,3 +82,43 @@ async def revoke_token(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e), detail=str(e),
) )
async def get_token_data(token: str, region: str, session: Session) -> Any:
try:
kc = await utils.keystone_client(
session=session,
region=region,
)
return await run_in_threadpool(kc.tokens.get_token_data, token)
except Unauthorized as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e),
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
async def get_user(id: str, region: str, session: Session) -> Any:
try:
kc = await utils.keystone_client(session=session, region=region)
return await run_in_threadpool(kc.users.get, id)
except Unauthorized as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e),
)
except NotFound as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)

View File

@ -94,6 +94,7 @@ class Profile(PayloadBase):
base_domains: Optional[List[str]] = Field(None, description="User base domains") base_domains: Optional[List[str]] = Field(None, description="User base domains")
endpoints: Optional[Dict[str, Any]] = Field(None, description="Keystone endpoints") endpoints: Optional[Dict[str, Any]] = Field(None, description="Keystone endpoints")
projects: Optional[Dict[str, Any]] = Field(None, description="User projects") projects: Optional[Dict[str, Any]] = Field(None, description="User projects")
default_project_id: Optional[str] = Field(None, description="User default project ID")
version: str = Field(..., description="Version") version: str = Field(..., description="Version")
def toPayLoad(self) -> Payload: def toPayLoad(self) -> Payload:

View File

@ -2757,6 +2757,11 @@
"type": "object", "type": "object",
"description": "User projects" "description": "User projects"
}, },
"default_project_id": {
"title": "Default Project Id",
"type": "string",
"description": "User default project ID"
},
"version": { "version": {
"title": "Version", "title": "Version",
"type": "string", "type": "string",