Commit 46e6b77b by Oleksandr Barabash

auth added

parent 4e92efa4
......@@ -5,6 +5,7 @@ import traceback
from datetime import datetime
from http import HTTPStatus
import marshmallow_dataclass as m_d
from aiohttp import web
from aiohttp.web import Request, Response, json_response
from aiohttp.web_fileresponse import FileResponse
......@@ -18,10 +19,12 @@ from marshmallow import EXCLUDE
from bots import TeamsMessagingExtensionsActionPreviewBot
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 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.teams_app_generator import TeamsAppGenerator
......@@ -155,21 +158,14 @@ async def v1_messages(request: Request) -> Response:
async def v1_health_check(_request: Request) -> Response:
""" Health check """
# TODO(s1z): Add checks here. DB, etc.
Log.i(TAG, "v1_health_check::ok")
key = None
container = None
try:
container = await COSMOS_CLIENT.get_conversations_container()
data = (await KEY_VAULT_CLIENT.get_secret("adminLogin")).value
key = await KEY_VAULT_CLIENT.create_key("pumpalot")
encrypted_data = await KEY_VAULT_CLIENT.encrypt(key, b"hello")
decrypted_data = await KEY_VAULT_CLIENT.decrypt(key, encrypted_data)
return Response(
body=json.dumps(dict(data=decrypted_data.decode("utf-8"))),
status=HTTPStatus.OK,
content_type="application/json"
)
_container = await COSMOS_CLIENT.get_conversations_container()
_data = (await KEY_VAULT_CLIENT.get_secret("adminLogin")).value
# key = await KEY_VAULT_CLIENT.create_key("pumpalot")
# encrypted_data = await KEY_VAULT_CLIENT.encrypt(key, b"hello")
# decrypted_data = await KEY_VAULT_CLIENT.decrypt(key, encrypted_data)
Log.i(TAG, "v1_health_check::ok")
return Response(status=HTTPStatus.OK)
except Exception as e:
Log.e(TAG, f"v1_health_check::error:{e}", sys.exc_info())
raise
......@@ -177,7 +173,6 @@ async def v1_health_check(_request: Request) -> Response:
async def get_app_zip(_request: Request) -> FileResponse:
""" Get zip file """
Log.i(TAG, "v1_health_check::ok")
await TeamsAppGenerator.generate_zip()
return FileResponse(path=TeamsAppConfig.zip_file)
......@@ -198,6 +193,20 @@ async def error_middleware(request, handler):
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.router.add_post("/api/v1/messages", v1_messages)
APP.router.add_post("/api/v1/notification", v1_notification)
......@@ -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/health-check", v1_health_check)
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)
......
......@@ -24,7 +24,6 @@ from entities.json.medx import MedX, MXTypes
from entities.json.notification import NotificationCosmos
from utils.card_helper import CardHelper
from utils.cosmos_client import CosmosClient, ItemNotFound
from utils.function import get_first_or_none
class TeamsMessagingExtensionsActionPreviewBot(TeamsActivityHandler):
......
......@@ -5,13 +5,23 @@ from azure.cosmos import PartitionKey
from utils.azure_key_vault_client import AzureKeyVaultClient
from utils.cosmos_client import CosmosClient
from utils.token_helper import TokenHelper
PROJECT_ROOT_PATH = os.path.dirname(os.path.abspath("__file__"))
ASSETS_PATH = os.path.join(PROJECT_ROOT_PATH, "assets")
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:
""" Teams app config """
teams_app_items = os.path.join(ASSETS_PATH, "teams_app_items")
......@@ -86,3 +96,4 @@ class CosmosDBConfig:
COSMOS_CLIENT = CosmosClient(CosmosDBConfig.HOST, CosmosDBConfig.KEY)
KEY_VAULT_CLIENT = AzureKeyVaultClient(AppConfig.CLIENT_ID,
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
stringcase==1.2.0
azure-keyvault-secrets==4.2.0
azure-keyvault-keys==4.3.1
pycryptodome==3.15.0
......@@ -2,6 +2,7 @@
import asyncio
import random
from concurrent.futures.thread import ThreadPoolExecutor
from datetime import datetime, timedelta
from typing import Awaitable
# noinspection PyPackageRequirements
......@@ -40,6 +41,14 @@ class AzureKeyVaultClient:
return self.execute_blocking(self.secret_client.set_secret, name,
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"]:
""" Async get secret """
return self.execute_blocking(self.secret_client.get_secret, name)
......@@ -52,17 +61,29 @@ class AzureKeyVaultClient:
""" Async create key """
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 """
keys = await self.execute_blocking(
self.key_client.list_properties_of_keys
)
keys = self.key_client.list_properties_of_keys()
all_keys = []
for key in keys:
all_keys.append(key)
random_key = random.choice(all_keys)
return await self.execute_blocking(self.key_client.get_key,
random_key.name)
return self.key_client.get_key(random_key.name)
def get_random_key(self) -> Awaitable["KeyVaultKey"]:
""" Async get random key """
......
""" Handy Functions """
from base64 import b64encode, b64decode
from typing import List, Optional, Dict
......@@ -7,3 +8,23 @@ def get_first_or_none(items: List) -> Optional[Dict[str, any]]:
if len(items) > 0:
return items[0]
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