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 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.responses import RedirectResponse
@ -26,7 +26,7 @@ from keystoneclient.client import Client as KeystoneClient
from skyline_apiserver import schemas
from skyline_apiserver.api import deps
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 (
get_endpoints,
get_project_scope_token,
@ -46,6 +46,19 @@ from skyline_apiserver.types import constants
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(
region: str,
domain: Optional[str] = None,
@ -53,7 +66,7 @@ async def _get_projects_and_unscope_token(
password: Optional[str] = None,
token: Optional[str] = None,
project_enabled: bool = False,
) -> Tuple[List[Any], str]:
) -> Tuple[List[Any], str, Union[str, None]]:
auth_url = await utils.get_endpoint(
region=region,
service="keystone",
@ -78,6 +91,9 @@ async def _get_projects_and_unscope_token(
session = Session(
auth=unscope_auth, verify=CONF.default.cafile, timeout=constants.DEFAULT_TIMEOUT
)
default_project_id = await _get_default_project_id(session, region)
unscope_client = KeystoneClient(
session=session,
endpoint=auth_url,
@ -93,7 +109,7 @@ async def _get_projects_and_unscope_token(
if not project_scope:
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:
@ -107,9 +123,13 @@ async def _patch_profile(profile: schemas.Profile, global_request_id: str) -> sc
)
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
)
else:
default_project_id = await _get_default_project_id(
get_system_session(), profile.region, user_id=profile.user.id
)
profile.projects = {
i.id: {
@ -121,6 +141,8 @@ async def _patch_profile(profile: schemas.Profile, global_request_id: str) -> sc
for i in projects
}
profile.default_project_id = default_project_id
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -151,7 +173,7 @@ async def login(
),
) -> schemas.Profile:
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,
domain=credential.domain,
username=credential.username,
@ -162,7 +184,7 @@ async def login(
project_scope_token = await get_project_scope_token(
keystone_token=unscope_token,
region=credential.region,
project_id=project_scope[0].id,
project_id=default_project_id or project_scope[0].id,
)
profile = await generate_profile(
@ -248,7 +270,7 @@ async def websso(
),
) -> RedirectResponse:
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,
token=token,
project_enabled=True,
@ -257,7 +279,7 @@ async def websso(
project_scope_token = await get_project_scope_token(
keystone_token=token,
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(

View File

@ -17,7 +17,7 @@ from __future__ import annotations
from typing import Any, Dict, Optional
from fastapi import HTTPException, status
from keystoneauth1.exceptions.http import Unauthorized
from keystoneauth1.exceptions.http import NotFound, Unauthorized
from keystoneauth1.session import Session
from starlette.concurrency import run_in_threadpool
@ -82,3 +82,43 @@ async def revoke_token(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
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")
endpoints: Optional[Dict[str, Any]] = Field(None, description="Keystone endpoints")
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")
def toPayLoad(self) -> Payload:

View File

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