diff --git a/.github/workflows/script/formatScan/pylint.sh b/.github/workflows/script/formatScan/pylint.sh index 57da15ba3d3..6158e322178 100644 --- a/.github/workflows/script/formatScan/pylint.sh +++ b/.github/workflows/script/formatScan/pylint.sh @@ -30,6 +30,12 @@ fi pip install git+https://github.com/EleutherAI/lm-evaluation-harness.git@83dbfbf6070324f3e5872f63e49d49ff7ef4c9b3 pip install accelerate nlpaug nltk schema optimum-intel==1.11.0 optimum==1.13.3 peft==0.6.2 +apt-get install libldap2-dev -y +apt-get install libsasl2-dev -y +export PIP_INDEX_URL=https://ubit-artifactory-or.intel.com/artifactory/api/pypi/iotg-rbhe-pypi-or-local/simple +export PIP_EXTRA_INDEX_URL=https://pypi.org/simple +pip install LDAPclient + echo "[DEBUG] list pipdeptree..." pip install pipdeptree pipdeptree diff --git a/.github/workflows/script/unitTest/run_unit_test_neuralchat.sh b/.github/workflows/script/unitTest/run_unit_test_neuralchat.sh index de5370b133d..2ebacd6ce05 100644 --- a/.github/workflows/script/unitTest/run_unit_test_neuralchat.sh +++ b/.github/workflows/script/unitTest/run_unit_test_neuralchat.sh @@ -84,10 +84,15 @@ function main() { apt-get install -y libgl1-mesa-glx apt-get install -y libgl1-mesa-dev apt-get install libsm6 libxext6 -y + apt-get install libldap2-dev -y + apt-get install libsasl2-dev -y wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.19_amd64.deb dpkg -i libssl1.1_1.1.1f-1ubuntu2.19_amd64.deb python -m pip install --upgrade --force-reinstall torch==2.1.0 pip install paddlepaddle==2.4.2 paddlenlp==2.5.2 paddlespeech==1.4.1 paddle2onnx==1.0.6 + export PIP_INDEX_URL=https://ubit-artifactory-or.intel.com/artifactory/api/pypi/iotg-rbhe-pypi-or-local/simple + export PIP_EXTRA_INDEX_URL=https://pypi.org/simple + pip install LDAPclient cd ${WORKING_DIR} || exit 1 echo "test on ${test_name}" if [[ $test_name == "PR-test" ]]; then diff --git a/intel_extension_for_transformers/neural_chat/requirements.txt b/intel_extension_for_transformers/neural_chat/requirements.txt index 27ccc939601..7d8cdef171f 100644 --- a/intel_extension_for_transformers/neural_chat/requirements.txt +++ b/intel_extension_for_transformers/neural_chat/requirements.txt @@ -1,8 +1,10 @@ accelerate +aiosqlite cchardet einops evaluate fastapi==0.103.2 +fastapi-users[sqlalchemy,oauth] fschat==0.2.32 git+https://github.com/EleutherAI/lm-evaluation-harness.git@cc9778fbe4fa1a709be2abed9deb6180fd40e7e2 huggingface_hub diff --git a/intel_extension_for_transformers/neural_chat/requirements_cpu.txt b/intel_extension_for_transformers/neural_chat/requirements_cpu.txt index 48a5329c47a..f77925c874f 100644 --- a/intel_extension_for_transformers/neural_chat/requirements_cpu.txt +++ b/intel_extension_for_transformers/neural_chat/requirements_cpu.txt @@ -1,12 +1,18 @@ --extra-index-url https://download.pytorch.org/whl/cpu +aiosqlite cchardet +cchardet +einops einops evaluate fastapi==0.103.2 +fastapi-users[sqlalchemy,oauth] fschat==0.2.32 git+https://github.com/EleutherAI/lm-evaluation-harness.git@cc9778fbe4fa1a709be2abed9deb6180fd40e7e2 intel_extension_for_pytorch==2.1.0 neural-compressor +neural-compressor +neural_speed neural_speed numpy==1.23.5 optimum @@ -14,6 +20,8 @@ optimum-intel peft==0.6.2 pydantic==1.10.13 python-dotenv +python-dotenv +python-multipart python-multipart rouge_score shortuuid diff --git a/intel_extension_for_transformers/neural_chat/requirements_hpu.txt b/intel_extension_for_transformers/neural_chat/requirements_hpu.txt index 6700cfa2c87..3953f3cd488 100644 --- a/intel_extension_for_transformers/neural_chat/requirements_hpu.txt +++ b/intel_extension_for_transformers/neural_chat/requirements_hpu.txt @@ -1,7 +1,9 @@ +aiosqlite cchardet einops evaluate fastapi==0.103.2 +fastapi-users[sqlalchemy,oauth] fschat==0.2.32 git+https://github.com/EleutherAI/lm-evaluation-harness.git@cc9778fbe4fa1a709be2abed9deb6180fd40e7e2 intel_extension_for_pytorch diff --git a/intel_extension_for_transformers/neural_chat/requirements_pc.txt b/intel_extension_for_transformers/neural_chat/requirements_pc.txt index 84edc0c4f70..4ac13422c4f 100644 --- a/intel_extension_for_transformers/neural_chat/requirements_pc.txt +++ b/intel_extension_for_transformers/neural_chat/requirements_pc.txt @@ -1,17 +1,25 @@ --extra-index-url https://download.pytorch.org/whl/cpu +aiosqlite cchardet +cchardet +einops einops evaluate fastapi==0.103.2 +fastapi-users[sqlalchemy,oauth] fschat==0.2.32 git+https://github.com/EleutherAI/lm-evaluation-harness.git@cc9778fbe4fa1a709be2abed9deb6180fd40e7e2 neural-compressor +neural-compressor +neural_speed numpy==1.23.5 optimum optimum-intel peft pydantic==1.10.13 python-dotenv +python-dotenv +python-multipart python-multipart rouge_score shortuuid diff --git a/intel_extension_for_transformers/neural_chat/requirements_xpu.txt b/intel_extension_for_transformers/neural_chat/requirements_xpu.txt index f2a980c0e9c..eae7d1cb7bb 100644 --- a/intel_extension_for_transformers/neural_chat/requirements_xpu.txt +++ b/intel_extension_for_transformers/neural_chat/requirements_xpu.txt @@ -1,7 +1,9 @@ +aiosqlite cchardet einops evaluate fastapi==0.103.2 +fastapi-users[sqlalchemy,oauth] fschat==0.2.32 neural-compressor numpy==1.23.5 diff --git a/intel_extension_for_transformers/neural_chat/server/config/__init__.py b/intel_extension_for_transformers/neural_chat/server/config/__init__.py new file mode 100644 index 00000000000..18896e7b549 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/config/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/intel_extension_for_transformers/neural_chat/server/config/config.py b/intel_extension_for_transformers/neural_chat/server/config/config.py new file mode 100644 index 00000000000..a333b9d3d90 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/config/config.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from dotenv import load_dotenv + +from functools import lru_cache + +from pydantic import BaseSettings + +from intel_extension_for_transformers.utils import logger + +load_dotenv() + + +class Settings(BaseSettings): + mysql_user: str = os.environ.get("MYSQL_USER", "root") + mysql_password: str = os.environ.get("MYSQL_PASSWORD") + mysql_host: str = os.environ.get("MYSQL_HOST", "localhost") + mysql_db: str = os.getenv("MYSQL_DB", "inc") + google_oauth_client_id: str = os.getenv("GOOGLE_OAUTH_CLIENT_ID", "") + google_oauth_client_secret: str = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", "") + github_oauth_client_id: str = os.getenv("GITHUB_OAUTH_CLIENT_ID", "") + github_oauth_client_secret: str = os.getenv("GITHUB_OAUTH_CLIENT_SECRET", "") + facebook_oauth_client_id: str = os.getenv("FACEBOOK_OAUTH_CLIENT_ID", "") + facebook_oauth_client_secret: str = os.getenv("FACEBOOK_OAUTH_CLIENT_SECRET", "") + microsoft_oauth_client_id: str = os.getenv("MICROSOFT_OAUTH_CLIENT_ID", "") + microsoft_oauth_client_secret: str = os.getenv("MICROSOFT_OAUTH_CLIENT_SECRET", "") + + +@lru_cache() +def get_settings() -> BaseSettings: + logger.info("Loading config settings from the environment...") + return Settings() diff --git a/intel_extension_for_transformers/neural_chat/server/database/__init__.py b/intel_extension_for_transformers/neural_chat/server/database/__init__.py new file mode 100644 index 00000000000..18896e7b549 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/database/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/intel_extension_for_transformers/neural_chat/server/database/user_db.py b/intel_extension_for_transformers/neural_chat/server/database/user_db.py new file mode 100644 index 00000000000..4a8759601e3 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/database/user_db.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import AsyncGenerator, List + +from fastapi import Depends +from fastapi_users.db import ( + SQLAlchemyBaseOAuthAccountTableUUID, + SQLAlchemyBaseUserTableUUID, + SQLAlchemyUserDatabase, +) +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import DeclarativeBase, Mapped, relationship, mapped_column +from sqlalchemy import String, Boolean, Column + +DATABASE_URL = "sqlite+aiosqlite:///users.db" + + +class Base(DeclarativeBase): + pass + + +class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): + pass + + +class User(SQLAlchemyBaseUserTableUUID, Base): + role = Column(String(length=50), default='user', nullable=False) + is_vipuser = Column(Boolean, default=False, nullable=False) + wwid = Column(String(length=50), nullable=True) + account = Column(String(length=50), nullable=True) + name = Column(String(length=255), nullable=True) + given_name = Column(String(length=255), nullable=True) + distinguished_name = Column(String(length=255), nullable=True) + idsid = Column(String(length=50), nullable=True) + generic = Column(Boolean, default=False, nullable=False) + SuperGroup = Column(String(length=50), nullable=True) + Group = Column(String(length=50), nullable=True) + Division = Column(String(length=50), nullable=True) + DivisionLong = Column(String(length=255), nullable=True) + CostCenterLong = Column(String(length=255), nullable=True) + mgrWWID = Column(String(length=50), nullable=True) + + oauth_accounts: Mapped[List[OAuthAccount]] = relationship( + "OAuthAccount", lazy="joined" + ) + + + +engine = create_async_engine(DATABASE_URL) +async_session_maker = async_sessionmaker(engine, expire_on_commit=False) + + +async def create_db_and_tables(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def get_async_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_maker() as session: + yield session + + +async def get_user_db(session: AsyncSession = Depends(get_async_session)): + yield SQLAlchemyUserDatabase(session, User, OAuthAccount) diff --git a/intel_extension_for_transformers/neural_chat/server/neuralchat_server.py b/intel_extension_for_transformers/neural_chat/server/neuralchat_server.py index f86afcb326b..de09f815fe7 100644 --- a/intel_extension_for_transformers/neural_chat/server/neuralchat_server.py +++ b/intel_extension_for_transformers/neural_chat/server/neuralchat_server.py @@ -21,14 +21,15 @@ import os import time from typing import List - +import asyncio import uvicorn import yaml import logging from yacs.config import CfgNode -from fastapi import FastAPI +from fastapi import FastAPI, Depends, HTTPException from fastapi import APIRouter +from fastapi_users import exceptions from starlette.middleware.cors import CORSMiddleware from .base_executor import BaseCommandExecutor from .server_commands import cli_server_register @@ -38,7 +39,10 @@ from ..chatbot import build_chatbot from ..plugins import plugins from transformers import BitsAndBytesConfig - +from .user.users import auth_backend, current_active_user, fastapi_users, UserManager, get_user_manager +from .schemas.user import UserCreate, UserRead, UserUpdate +from .database.user_db import User, create_db_and_tables +from .user.users import SECRET, google_oauth_client, github_oauth_client, facebook_oauth_client, microsoft_oauth_client __all__ = ['NeuralChatServerExecutor'] @@ -54,7 +58,11 @@ api_router = APIRouter() - +@app.on_event("startup") +async def startup_event(): + # user database init + logging.info("Starting init user database...") + await create_db_and_tables() def get_config(config_file: str): @@ -71,6 +79,55 @@ def get_config(config_file: str): return config +def setup_authentication_router(): + app.include_router( + fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] + ) + app.include_router( + fastapi_users.get_register_router(UserRead, UserCreate), + prefix="/auth", + tags=["auth"], + ) + app.include_router( + fastapi_users.get_reset_password_router(), + prefix="/auth", + tags=["auth"], + ) + app.include_router( + fastapi_users.get_verify_router(UserRead), + prefix="/auth", + tags=["auth"], + ) + app.include_router( + fastapi_users.get_users_router(UserRead, UserUpdate), + prefix="/users", + tags=["users"], + ) + + app.include_router( + fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET), + prefix="/auth/google", + tags=["auth"], + ) + + app.include_router( + fastapi_users.get_oauth_router(github_oauth_client, auth_backend, SECRET), + prefix="/auth/github", + tags=["auth"], + ) + + app.include_router( + fastapi_users.get_oauth_router(facebook_oauth_client, auth_backend, SECRET), + prefix="/auth/facebook", + tags=["auth"], + ) + + app.include_router( + fastapi_users.get_oauth_router(microsoft_oauth_client, auth_backend, SECRET), + prefix="/auth/microsoft", + tags=["auth"], + ) + @cli_server_register(name='neuralchat_server.start', description='Start the service') class NeuralChatServerExecutor(BaseCommandExecutor): def __init__(self): @@ -89,6 +146,7 @@ def __init__(self): action="store", help="log file", default="./log/neuralchat.log") + asyncio.run(create_db_and_tables()) def init(self, config): """System initialization. @@ -211,6 +269,8 @@ def init(self, config): from .restful.api import setup_router api_router = setup_router(api_list, enable_llm=False) app.include_router(api_router) + # include authentication router + setup_authentication_router() return True # chatbot as service else: @@ -318,6 +378,8 @@ def init(self, config): from .restful.api import setup_router api_router = setup_router(api_list, self.chatbot, True, use_deepspeed, world_size, host, port) app.include_router(api_router) + # include authentication router + setup_authentication_router() return True @@ -343,3 +405,72 @@ def __call__(self, uvicorn.run(app, host=config.host, port=config.port) except Exception as e: print(f"Error starting uvicorn: {str(e)}") + +@app.get("/authenticated-route") +async def authenticated_route(user: User = Depends(current_active_user)): + return {"message": f"Hello {user.email}!"} + + +@app.post("/intel/login") +async def login( + credentials: dict, + user_manager: UserManager = Depends(get_user_manager) +): + try: + username = credentials.get("account") + password = credentials.get("password") + + from LDAPclient import IntelLDAP # pylint: disable=E0611, E0401 + ldap_client = IntelLDAP(user=username, password=password) + user = None + idsid = username[len("CCR\\"):] + user = ldap_client.get_user(idsid) + + if user is None: + raise HTTPException(status_code=401, detail="LDAP authentication failed") + + # Authenticate the user with the UserManager + try: + # Authenticate the user with the UserManager + user = await user_manager.get_by_email(user["email_address"]) + except exceptions.UserNotExists: + user_create = UserCreate(account=username, + password=password, + wwid=user["wwid"], + email=user["email_address"], + idsid=user["idsid"], + name=user["name"], + given_name=user["given_name"], + distinguished_name=user["distinguished_name"], + generic=user["generic"], + SuperGroup=user["SuperGroup"], + Group=user["Group"], + Division=user["Division"], + DivisionLong=user["DivisionLong"], + CostCenterLong=user["CostCenterLong"], + mgrWWID=user["mgrWWID"], + ) + user = await user_manager.create(user_create) + + user_dict = { + "id": str(user.id), + "role": user.role, + "is_vipuser": user.is_vipuser, + "wwid": user.wwid, + "email_address": user.email, + "account": user.account, + "name": user.name, + "given_name": user.given_name, + "distinguished_name": user.distinguished_name, + "idsid": user.idsid, + "generic": user.generic, + "SuperGroup": user.SuperGroup, + "Group": user.Group, + "Division": user.Division, + "DivisionLong": user.DivisionLong, + "CostCenterLong": user.CostCenterLong, + "mgrWWID": user.mgrWWID, + } + return {"msg": "Login successful", "user_info": user_dict} + except HTTPException as e: + return {"msg": "Login failed", "error_detail": e.detail} diff --git a/intel_extension_for_transformers/neural_chat/server/schemas/__init__.py b/intel_extension_for_transformers/neural_chat/server/schemas/__init__.py new file mode 100644 index 00000000000..18896e7b549 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/schemas/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/intel_extension_for_transformers/neural_chat/server/schemas/user.py b/intel_extension_for_transformers/neural_chat/server/schemas/user.py new file mode 100644 index 00000000000..f0a85ff71fc --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/schemas/user.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +from fastapi_users import schemas +from typing import Optional + + +class UserRead(schemas.BaseUser[uuid.UUID]): + role: str + is_vipuser: bool = False + wwid: Optional[str] = None + account: Optional[str] = None + name: Optional[str] = None + given_name: Optional[str] = None + distinguished_name: Optional[str] = None + idsid: Optional[str] = None + generic: bool = False + SuperGroup: Optional[str] = None + Group: Optional[str] = None + Division: Optional[str] = None + DivisionLong: Optional[str] = None + CostCenterLong: Optional[str] = None + mgrWWID: Optional[str] = None + +class UserCreate(schemas.BaseUserCreate): + role: Optional[str] = 'user' + is_vipuser: bool = False + wwid: Optional[str] = None + account: Optional[str] = None + name: Optional[str] = None + given_name: Optional[str] = None + distinguished_name: Optional[str] = None + idsid: Optional[str] = None + generic: bool = False + SuperGroup: Optional[str] = None + Group: Optional[str] = None + Division: Optional[str] = None + DivisionLong: Optional[str] = None + CostCenterLong: Optional[str] = None + mgrWWID: Optional[str] = None + +class UserUpdate(schemas.BaseUserUpdate): + role: Optional[str] + is_vipuser: Optional[bool] + wwid: Optional[str] = None + account: Optional[str] = None + name: Optional[str] = None + given_name: Optional[str] = None + distinguished_name: Optional[str] = None + idsid: Optional[str] = None + generic: bool = False + SuperGroup: Optional[str] = None + Group: Optional[str] = None + Division: Optional[str] = None + DivisionLong: Optional[str] = None + CostCenterLong: Optional[str] = None + mgrWWID: Optional[str] = None diff --git a/intel_extension_for_transformers/neural_chat/server/user/__init__.py b/intel_extension_for_transformers/neural_chat/server/user/__init__.py new file mode 100644 index 00000000000..18896e7b549 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/user/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/intel_extension_for_transformers/neural_chat/server/user/users.py b/intel_extension_for_transformers/neural_chat/server/user/users.py new file mode 100644 index 00000000000..5fb340bd4b7 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/server/user/users.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid +from typing import Optional, Union, Dict, Any + +from fastapi import Depends, Request, Response +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, InvalidPasswordException +from fastapi_users.authentication import ( + AuthenticationBackend, + BearerTransport, + JWTStrategy, +) +from fastapi_users.db import SQLAlchemyUserDatabase +from httpx_oauth.clients.google import GoogleOAuth2 +from httpx_oauth.clients.github import GitHubOAuth2 +from httpx_oauth.clients.facebook import FacebookOAuth2 +from httpx_oauth.clients.microsoft import MicrosoftGraphOAuth2 + +from ..database.user_db import User, get_user_db +from ..schemas.user import UserCreate + +from ..config.config import get_settings + +from intel_extension_for_transformers.utils import logger + +global_settings = get_settings() + +SECRET = "SECRET" + +google_oauth_client = GoogleOAuth2( + global_settings.google_oauth_client_id, + global_settings.google_oauth_client_secret +) + +github_oauth_client = GitHubOAuth2( + global_settings.github_oauth_client_id, + global_settings.github_oauth_client_secret +) + +facebook_oauth_client = FacebookOAuth2( + global_settings.facebook_oauth_client_id, + global_settings.facebook_oauth_client_secret +) + +microsoft_oauth_client = MicrosoftGraphOAuth2( + global_settings.microsoft_oauth_client_id, + global_settings.microsoft_oauth_client_secret +) + +class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): + reset_password_token_secret = SECRET + verification_token_secret = SECRET + + async def validate_password( + self, + password: str, + user: Union[UserCreate, User], + ) -> None: + if len(password) < 8: + raise InvalidPasswordException( + reason="Password should be at least 8 characters" + ) + if user.email in password: + raise InvalidPasswordException( + reason="Password should not contain e-mail" + ) + + async def on_after_register(self, user: User, request: Optional[Request] = None): + logger.info(f"User {user.id} has registered.") + + async def on_after_forgot_password( + self, user: User, token: str, request: Optional[Request] = None + ): + logger.info(f"User {user.id} has forgot their password. Reset token: {token}") + + async def on_after_update( + self, + user: User, + update_dict: Dict[str, Any], + request: Optional[Request] = None, + ): + logger.info(f"User {user.id} has been updated with {update_dict}.") + + async def on_after_login( + self, + user: User, + request: Optional[Request] = None, + response: Optional[Response] = None, + ): + logger.info(f"User {user.id} logged in.") + + async def on_after_request_verify( + self, user: User, token: str, request: Optional[Request] = None + ): + logger.info(f"Verification requested for user {user.id}. Verification token: {token}") + + async def on_after_verify( + self, user: User, request: Optional[Request] = None + ): + logger.info(f"User {user.id} has been verified") + + async def on_after_reset_password(self, user: User, request: Optional[Request] = None): + logger.info(f"User {user.id} has reset their password.") + + async def on_before_delete(self, user: User, request: Optional[Request] = None): + logger.info(f"User {user.id} is going to be deleted") + + async def on_after_delete(self, user: User, request: Optional[Request] = None): + logger.info(f"User {user.id} is successfully deleted") + + async def get_oauth2_client(self, provider: str): + if provider == "google": + return google_oauth_client + elif provider == "github": + return github_oauth_client + elif provider == "facebook": + return facebook_oauth_client + elif provider == "microsoft": + return microsoft_oauth_client + else: + raise ValueError("Unsupported OAuth provider") + + async def get_oauth2_login_url(self, provider: str, state: str): + client = await self.get_oauth2_client(provider) + return client.get_authorization_url(state=state) + + async def on_after_login_with_oauth2( + self, user: User, provider: str, request: Optional[Request] = None + ): + logger.info(f"User {user.id} logged in with {provider.capitalize()}.") + +async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): + yield UserManager(user_db) + + +bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") + + +def get_jwt_strategy() -> JWTStrategy: + return JWTStrategy(secret=SECRET, lifetime_seconds=3600) + + +auth_backend = AuthenticationBackend( + name="jwt", + transport=bearer_transport, + get_strategy=get_jwt_strategy, + ) + +fastapi_users = FastAPIUsers[User, uuid.UUID]( + get_user_manager, [auth_backend] +) + +current_active_user = fastapi_users.current_user(active=True) diff --git a/intel_extension_for_transformers/neural_chat/tests/ci/server/test_authentication.py b/intel_extension_for_transformers/neural_chat/tests/ci/server/test_authentication.py new file mode 100644 index 00000000000..e1908b306d7 --- /dev/null +++ b/intel_extension_for_transformers/neural_chat/tests/ci/server/test_authentication.py @@ -0,0 +1,70 @@ +# !/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import contextlib +import asyncio +import os +from fastapi.testclient import TestClient +from intel_extension_for_transformers.neural_chat.server.neuralchat_server import app, setup_authentication_router +from intel_extension_for_transformers.neural_chat.server.database.user_db import create_db_and_tables, get_async_session, get_user_db +from intel_extension_for_transformers.neural_chat.server.schemas.user import UserCreate +from intel_extension_for_transformers.neural_chat.server.user.users import get_user_manager +from fastapi_users.exceptions import UserAlreadyExists + +get_async_session_context = contextlib.asynccontextmanager(get_async_session) +get_user_db_context = contextlib.asynccontextmanager(get_user_db) +get_user_manager_context = contextlib.asynccontextmanager(get_user_manager) + + +async def create_user(email: str, password: str, is_superuser: bool = False): + try: + async with get_async_session_context() as session: + async with get_user_db_context(session) as user_db: + async with get_user_manager_context(user_db) as user_manager: + user = await user_manager.create( + UserCreate( + email=email, password=password, is_superuser=is_superuser + ) + ) + print(f"User created {user}") + except UserAlreadyExists: + print(f"User {email} already exists") + + +class TestAuthenticationRouter(unittest.TestCase): + def setUp(self): + setup_authentication_router() + asyncio.run(create_db_and_tables()) + self.client = TestClient(app) + asyncio.run(create_user("test_user@example.com", "test_password")) + + def tearDown(self) -> None: + if os.path.exists('users.db'): + os.remove('users.db') + return super().tearDown() + + def test_register_router(self): + response = self.client.post("/auth/register", json={"email": "new_user@example.com", "password": "password"}) + self.assertEqual(response.status_code, 201) + + def test_auth_jwt_router(self): + response = self.client.post("/auth/jwt/login", data={"username": "test_user@example.com", "password": "test_password"}) + self.assertEqual(response.status_code, 200) + +if __name__ == "__main__": + unittest.main() diff --git a/intel_extension_for_transformers/neural_chat/tests/requirements.txt b/intel_extension_for_transformers/neural_chat/tests/requirements.txt index 5ac25cea1d6..e9fe4a60b03 100644 --- a/intel_extension_for_transformers/neural_chat/tests/requirements.txt +++ b/intel_extension_for_transformers/neural_chat/tests/requirements.txt @@ -1,3 +1,4 @@ +aiosqlite av basicsr==1.4.2 beautifulsoup4 @@ -11,6 +12,7 @@ exifread face_alignment==1.3.5 facexlib @ git+https://github.com/Spycsh/facexlib@master fastapi==0.103.2 +fastapi-users[sqlalchemy,oauth] ffmpeg-python==0.2.0 fschat==0.2.32 gfpgan @@ -31,6 +33,7 @@ librosa markdown neural-compressor neural_speed +neural_speed num2words numba numpy==1.23.5