Commit 46e6b77b by Oleksandr Barabash

auth added

parent 4e92efa4
...@@ -5,6 +5,7 @@ import traceback ...@@ -5,6 +5,7 @@ import traceback
from datetime import datetime from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
import marshmallow_dataclass as m_d
from aiohttp import web from aiohttp import web
from aiohttp.web import Request, Response, json_response from aiohttp.web import Request, Response, json_response
from aiohttp.web_fileresponse import FileResponse from aiohttp.web_fileresponse import FileResponse
...@@ -18,10 +19,12 @@ from marshmallow import EXCLUDE ...@@ -18,10 +19,12 @@ from marshmallow import EXCLUDE
from bots import TeamsMessagingExtensionsActionPreviewBot from bots import TeamsMessagingExtensionsActionPreviewBot
from bots.exceptions import ConversationNotFound, DataParsingError from bots.exceptions import ConversationNotFound, DataParsingError
from config import AppConfig, COSMOS_CLIENT, KEY_VAULT_CLIENT, TeamsAppConfig from config import AppConfig, COSMOS_CLIENT, KEY_VAULT_CLIENT, TeamsAppConfig, \
TOKEN_HELPER
from entities.json.admin_user import AdminUser
from entities.json.notification import Notification from entities.json.notification import Notification
from utils.cosmos_client import ItemNotFound from utils.cosmos_client import ItemNotFound
from utils.json_func import json_loads from utils.json_func import json_loads, json_dumps
from utils.log import Log from utils.log import Log
from utils.teams_app_generator import TeamsAppGenerator from utils.teams_app_generator import TeamsAppGenerator
...@@ -155,21 +158,14 @@ async def v1_messages(request: Request) -> Response: ...@@ -155,21 +158,14 @@ async def v1_messages(request: Request) -> Response:
async def v1_health_check(_request: Request) -> Response: async def v1_health_check(_request: Request) -> Response:
""" Health check """ """ Health check """
# TODO(s1z): Add checks here. DB, etc.
Log.i(TAG, "v1_health_check::ok")
key = None
container = None
try: try:
container = await COSMOS_CLIENT.get_conversations_container() _container = await COSMOS_CLIENT.get_conversations_container()
data = (await KEY_VAULT_CLIENT.get_secret("adminLogin")).value _data = (await KEY_VAULT_CLIENT.get_secret("adminLogin")).value
key = await KEY_VAULT_CLIENT.create_key("pumpalot") # key = await KEY_VAULT_CLIENT.create_key("pumpalot")
encrypted_data = await KEY_VAULT_CLIENT.encrypt(key, b"hello") # encrypted_data = await KEY_VAULT_CLIENT.encrypt(key, b"hello")
decrypted_data = await KEY_VAULT_CLIENT.decrypt(key, encrypted_data) # decrypted_data = await KEY_VAULT_CLIENT.decrypt(key, encrypted_data)
return Response( Log.i(TAG, "v1_health_check::ok")
body=json.dumps(dict(data=decrypted_data.decode("utf-8"))), return Response(status=HTTPStatus.OK)
status=HTTPStatus.OK,
content_type="application/json"
)
except Exception as e: except Exception as e:
Log.e(TAG, f"v1_health_check::error:{e}", sys.exc_info()) Log.e(TAG, f"v1_health_check::error:{e}", sys.exc_info())
raise raise
...@@ -177,7 +173,6 @@ async def v1_health_check(_request: Request) -> Response: ...@@ -177,7 +173,6 @@ async def v1_health_check(_request: Request) -> Response:
async def get_app_zip(_request: Request) -> FileResponse: async def get_app_zip(_request: Request) -> FileResponse:
""" Get zip file """ """ Get zip file """
Log.i(TAG, "v1_health_check::ok")
await TeamsAppGenerator.generate_zip() await TeamsAppGenerator.generate_zip()
return FileResponse(path=TeamsAppConfig.zip_file) return FileResponse(path=TeamsAppConfig.zip_file)
...@@ -198,6 +193,20 @@ async def error_middleware(request, handler): ...@@ -198,6 +193,20 @@ async def error_middleware(request, handler):
body=json.dumps({"error": message})) body=json.dumps({"error": message}))
async def v1_auth(request: Request) -> Response:
""" Admin Auth """
if "application/json" in request.headers["Content-Type"]:
body = await request.json()
else:
return Response(status=HTTPStatus.UNSUPPORTED_MEDIA_TYPE)
admin_user = AdminUser.Schema(exclude=EXCLUDE).load(body)
if admin_user.login and admin_user.password:
result = await TOKEN_HELPER.do_auth(admin_user)
if result is not None:
return Response(status=HTTPStatus.OK, body=json_dumps(result))
return Response(status=HTTPStatus.FORBIDDEN)
APP = web.Application(middlewares=[error_middleware]) APP = web.Application(middlewares=[error_middleware])
APP.router.add_post("/api/v1/messages", v1_messages) APP.router.add_post("/api/v1/messages", v1_messages)
APP.router.add_post("/api/v1/notification", v1_notification) APP.router.add_post("/api/v1/notification", v1_notification)
...@@ -206,6 +215,7 @@ APP.router.add_get("/api/v1/notification/{notification_id}", ...@@ -206,6 +215,7 @@ APP.router.add_get("/api/v1/notification/{notification_id}",
APP.router.add_get("/api/v1/initiations/{notification_id}", v1_get_initiations) APP.router.add_get("/api/v1/initiations/{notification_id}", v1_get_initiations)
APP.router.add_get("/api/v1/health-check", v1_health_check) APP.router.add_get("/api/v1/health-check", v1_health_check)
APP.router.add_get("/{}".format(TeamsAppConfig.zip_name), get_app_zip) APP.router.add_get("/{}".format(TeamsAppConfig.zip_name), get_app_zip)
APP.router.add_post("/api/v1/auth", v1_auth)
BOT.add_web_app(APP) BOT.add_web_app(APP)
......
...@@ -24,7 +24,6 @@ from entities.json.medx import MedX, MXTypes ...@@ -24,7 +24,6 @@ from entities.json.medx import MedX, MXTypes
from entities.json.notification import NotificationCosmos from entities.json.notification import NotificationCosmos
from utils.card_helper import CardHelper from utils.card_helper import CardHelper
from utils.cosmos_client import CosmosClient, ItemNotFound from utils.cosmos_client import CosmosClient, ItemNotFound
from utils.function import get_first_or_none
class TeamsMessagingExtensionsActionPreviewBot(TeamsActivityHandler): class TeamsMessagingExtensionsActionPreviewBot(TeamsActivityHandler):
......
...@@ -5,13 +5,23 @@ from azure.cosmos import PartitionKey ...@@ -5,13 +5,23 @@ from azure.cosmos import PartitionKey
from utils.azure_key_vault_client import AzureKeyVaultClient from utils.azure_key_vault_client import AzureKeyVaultClient
from utils.cosmos_client import CosmosClient from utils.cosmos_client import CosmosClient
from utils.token_helper import TokenHelper
PROJECT_ROOT_PATH = os.path.dirname(os.path.abspath("__file__")) PROJECT_ROOT_PATH = os.path.dirname(os.path.abspath("__file__"))
ASSETS_PATH = os.path.join(PROJECT_ROOT_PATH, "assets") ASSETS_PATH = os.path.join(PROJECT_ROOT_PATH, "assets")
CARDS_PATH = os.path.join(ASSETS_PATH, "cards") CARDS_PATH = os.path.join(ASSETS_PATH, "cards")
class Auth:
""" Auth type """
RS256 = "RS256"
HS256 = "HS256"
CURRENT = RS256
ADMIN_LOGIN_SECRET = "adminLogin"
ADMIN_PASSW_SECRET = "adminPassword"
class TeamsAppConfig: class TeamsAppConfig:
""" Teams app config """ """ Teams app config """
teams_app_items = os.path.join(ASSETS_PATH, "teams_app_items") teams_app_items = os.path.join(ASSETS_PATH, "teams_app_items")
...@@ -86,3 +96,4 @@ class CosmosDBConfig: ...@@ -86,3 +96,4 @@ class CosmosDBConfig:
COSMOS_CLIENT = CosmosClient(CosmosDBConfig.HOST, CosmosDBConfig.KEY) COSMOS_CLIENT = CosmosClient(CosmosDBConfig.HOST, CosmosDBConfig.KEY)
KEY_VAULT_CLIENT = AzureKeyVaultClient(AppConfig.CLIENT_ID, KEY_VAULT_CLIENT = AzureKeyVaultClient(AppConfig.CLIENT_ID,
AppConfig.KEY_VAULT) AppConfig.KEY_VAULT)
TOKEN_HELPER = TokenHelper(KEY_VAULT_CLIENT)
""" Notification Schema Implementation """
import marshmallow_dataclass
from entities.json.camel_case_schema import CamelCaseSchema
@marshmallow_dataclass.dataclass(base_schema=CamelCaseSchema)
class AdminUser:
""" Notification Schema """
login: str
password: str
...@@ -9,4 +9,4 @@ marshmallow-dataclass==8.5.8 ...@@ -9,4 +9,4 @@ marshmallow-dataclass==8.5.8
stringcase==1.2.0 stringcase==1.2.0
azure-keyvault-secrets==4.2.0 azure-keyvault-secrets==4.2.0
azure-keyvault-keys==4.3.1 azure-keyvault-keys==4.3.1
pycryptodome==3.15.0
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import asyncio import asyncio
import random import random
from concurrent.futures.thread import ThreadPoolExecutor from concurrent.futures.thread import ThreadPoolExecutor
from datetime import datetime, timedelta
from typing import Awaitable from typing import Awaitable
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
...@@ -40,6 +41,14 @@ class AzureKeyVaultClient: ...@@ -40,6 +41,14 @@ class AzureKeyVaultClient:
return self.execute_blocking(self.secret_client.set_secret, name, return self.execute_blocking(self.secret_client.set_secret, name,
value) value)
def get_secret_bl(self, name: str) -> "KeyVaultSecret":
""" Get secret blocking """
return self.secret_client.get_secret(name)
def set_secret_bl(self, name: str, value: str) -> "KeyVaultSecret":
""" Get secret blocking """
return self.secret_client.set_secret(name, value)
def get_secret(self, name: str) -> Awaitable["KeyVaultSecret"]: def get_secret(self, name: str) -> Awaitable["KeyVaultSecret"]:
""" Async get secret """ """ Async get secret """
return self.execute_blocking(self.secret_client.get_secret, name) return self.execute_blocking(self.secret_client.get_secret, name)
...@@ -52,17 +61,29 @@ class AzureKeyVaultClient: ...@@ -52,17 +61,29 @@ class AzureKeyVaultClient:
""" Async create key """ """ Async create key """
return self.execute_blocking(self.key_client.create_rsa_key, name) return self.execute_blocking(self.key_client.create_rsa_key, name)
async def get_random_key_bl(self) -> KeyVaultKey: def get_or_create_random_key_bl(self, quantity=10) -> KeyVaultKey:
""" Get or Create random key """
random_key = None
try:
random_key = self.get_random_key_bl()
except IndexError:
# create keys here
for _ in range(quantity):
name = "%i" % (datetime.now().timestamp() * 1000000)
expires_on = datetime.now() + timedelta(days=7)
random_key = self.key_client.create_rsa_key(
name, expires_on=expires_on
)
return random_key
def get_random_key_bl(self) -> KeyVaultKey:
""" Blocking get random key """ """ Blocking get random key """
keys = await self.execute_blocking( keys = self.key_client.list_properties_of_keys()
self.key_client.list_properties_of_keys
)
all_keys = [] all_keys = []
for key in keys: for key in keys:
all_keys.append(key) all_keys.append(key)
random_key = random.choice(all_keys) random_key = random.choice(all_keys)
return await self.execute_blocking(self.key_client.get_key, return self.key_client.get_key(random_key.name)
random_key.name)
def get_random_key(self) -> Awaitable["KeyVaultKey"]: def get_random_key(self) -> Awaitable["KeyVaultKey"]:
""" Async get random key """ """ Async get random key """
......
""" Handy Functions """ """ Handy Functions """
from base64 import b64encode, b64decode
from typing import List, Optional, Dict from typing import List, Optional, Dict
...@@ -7,3 +8,23 @@ def get_first_or_none(items: List) -> Optional[Dict[str, any]]: ...@@ -7,3 +8,23 @@ def get_first_or_none(items: List) -> Optional[Dict[str, any]]:
if len(items) > 0: if len(items) > 0:
return items[0] return items[0]
return None return None
def b64encode_str(data: str, encoding="utf-8") -> str:
""" Decode base64 str and return decoded string """
return b64encode_np(data.encode(encoding)).decode(encoding)
def b64decode_str(data: str, encoding="utf-8") -> str:
""" Decode base64 str and return decoded string """
return b64decode_np(data.encode(encoding)).decode(encoding)
def b64encode_np(data: bytes) -> bytes:
""" B64 without paddings """
return b64encode(data).replace(b"=", b'')
def b64decode_np(data: bytes) -> bytes:
""" B64 without paddings """
return b64decode(data + b'===')
""" Token Helper """
import asyncio
from calendar import timegm
from concurrent.futures.thread import ThreadPoolExecutor
from datetime import datetime, timedelta
from typing import Dict, Union
from Crypto.Hash import SHA256
from entities.json.admin_user import AdminUser
from utils.azure_key_vault_client import AzureKeyVaultClient
from utils.functions import b64encode_str, b64encode_np
from utils.json_func import json_dumps
class MimeTypes:
""" Token Types selection """
JWT = "jwt"
class TokenHelper:
""" Token Helper implementation """
def __init__(self, azure_cli: AzureKeyVaultClient):
self.azure_cli = azure_cli
self.executor = ThreadPoolExecutor(10)
self.io_loop = asyncio.get_event_loop()
def sign_token_bl(self, header: Dict[str, Union[str, int]],
body: Dict[str, Union[str, int]],
alg: str) -> str:
""" Sign token and return "{token}.{signature}" """
from config import Auth
if alg == Auth.RS256:
""" RSA signature with SHA-256 """
key = self.azure_cli.get_or_create_random_key_bl()
header.update(dict(kid=key.name))
token_unsigned = "{}.{}".format(b64encode_str(json_dumps(header)),
b64encode_str(json_dumps(body)))
signature = SHA256.new(token_unsigned.encode("utf-8")).digest()
signature_encrypted = self.azure_cli.encrypt_bl(key, signature)
signature_b64 = b64encode_np(signature_encrypted).decode("utf-8")
return "{}.{}".format(token_unsigned, signature_b64)
elif alg == Auth.HS256:
""" HMAC with SHA-256 (HS256) """
pass
raise NotImplementedError("'{}' ALGORITHM ISN'T SUPPORTED".format(alg))
def create_token_bl(self, login: str, ttl_seconds=3600) -> str:
""" Create JWT token for the User, blocking """
from config import Auth
date = datetime.utcnow() + timedelta(seconds=ttl_seconds)
exp = timegm(date.utctimetuple())
alg = Auth.CURRENT
jwt_head = dict(typ=MimeTypes.JWT, alg=alg)
jwt_body = dict(sub=login, exp=exp)
token_signed = self.sign_token_bl(jwt_head, jwt_body, alg)
return token_signed
def do_auth_bl(self, user: AdminUser):
""" Perform Auth blocking """
from config import Auth
kv_login = self.azure_cli.get_secret_bl(Auth.ADMIN_LOGIN_SECRET).value
kv_passw = self.azure_cli.get_secret_bl(Auth.ADMIN_PASSW_SECRET).value
if user.login == kv_login and user.password == kv_passw:
ttl = 3600
token = self.create_token_bl(user.login, ttl)
return dict(tokenType="Bearer",
expiresIn=ttl,
accessToken=token)
raise None
def do_auth(self, user: AdminUser):
""" Perform auth async """
return self.io_loop.run_in_executor(self.executor, self.do_auth_bl,
user)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment