parent
a2e6088216
commit
91690f73e2
@ -0,0 +1,7 @@
|
|||||||
|
from calculate.logging import dictLogConfig
|
||||||
|
|
||||||
|
|
||||||
|
config = {"socket_path": "./input.sock",
|
||||||
|
"variables_path": "calculate/variables",
|
||||||
|
"commands_path": "calculate/commands",
|
||||||
|
"logger_config": dictLogConfig}
|
@ -0,0 +1,14 @@
|
|||||||
|
from os import environ
|
||||||
|
from databases import Database
|
||||||
|
|
||||||
|
|
||||||
|
TESTING = bool(environ.get("TESTING", False))
|
||||||
|
|
||||||
|
|
||||||
|
if TESTING:
|
||||||
|
DATABASE_URL = "sqlite:///tests/server/testfiles/test.db"
|
||||||
|
else:
|
||||||
|
# Временно.
|
||||||
|
DATABASE_URL = "sqlite:///calculate/server/tmp.db"
|
||||||
|
|
||||||
|
database = Database(DATABASE_URL)
|
@ -0,0 +1,34 @@
|
|||||||
|
from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
|
||||||
|
|
||||||
|
|
||||||
|
metadata = MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
users_table: Table = Table("users",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True),
|
||||||
|
Column("login",
|
||||||
|
String(20),
|
||||||
|
unique=True,
|
||||||
|
nullable=False,
|
||||||
|
index=True),
|
||||||
|
Column("password",
|
||||||
|
String(77),
|
||||||
|
nullable=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
rights_table: Table = Table("rights",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True),
|
||||||
|
Column("name",
|
||||||
|
String(20),
|
||||||
|
unique=True,
|
||||||
|
nullable=False,
|
||||||
|
index=True),
|
||||||
|
Column("description", String(40)))
|
||||||
|
|
||||||
|
users_rights: Table = Table("users_rights",
|
||||||
|
metadata,
|
||||||
|
Column("user_id", ForeignKey("users.id")),
|
||||||
|
Column("right_id", ForeignKey("rights.id"))
|
||||||
|
)
|
@ -0,0 +1,56 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from ..utils.dependencies import right_checkers
|
||||||
|
|
||||||
|
from ..server_data import ServerData
|
||||||
|
|
||||||
|
|
||||||
|
data = ServerData()
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/commands", tags=["Commands management"],
|
||||||
|
dependencies=[Depends(right_checkers["read"])])
|
||||||
|
async def get_commands() -> dict:
|
||||||
|
'''Обработчик, отвечающий на запросы списка команд.'''
|
||||||
|
response = {}
|
||||||
|
for command_id, command_object in data.commands.items():
|
||||||
|
response.update({command_id: {"title": command_object.title,
|
||||||
|
"category": command_object.category,
|
||||||
|
"icon": command_object.icon,
|
||||||
|
"command": command_object.command}})
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/commands/{cid}", tags=["Commands management"],
|
||||||
|
dependencies=[Depends(right_checkers["read"])])
|
||||||
|
async def get_command(cid: int) -> dict:
|
||||||
|
'''Обработчик запросов списка команд.'''
|
||||||
|
if cid not in data.commands_instances:
|
||||||
|
# TODO добавить какую-то обработку ошибки.
|
||||||
|
pass
|
||||||
|
return {'id': cid,
|
||||||
|
'name': f'command_{cid}'}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/commands/{cid}/groups", tags=["Commands management"],
|
||||||
|
dependencies=[Depends(right_checkers["read"])])
|
||||||
|
async def get_command_parameters_groups(cid: int) -> dict:
|
||||||
|
'''Обработчик запросов на получение групп параметров указанной команды.'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/commands/{cid}/parameters", tags=["Commands management"],
|
||||||
|
dependencies=[Depends(right_checkers["read"])])
|
||||||
|
async def get_command_parameters(cid: int) -> dict:
|
||||||
|
'''Обработчик запросов на получение параметров указанной команды.'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/commands/{command_id}", tags=["Commands management"],
|
||||||
|
dependencies=[Depends(right_checkers["write"])])
|
||||||
|
async def post_command(command_id: str) -> int:
|
||||||
|
if command_id not in data.commands:
|
||||||
|
# TODO добавить какую-то обработку ошибки.
|
||||||
|
pass
|
||||||
|
return
|
@ -0,0 +1,13 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigSchema(BaseModel):
|
||||||
|
socket_path: str
|
||||||
|
variables_path: str
|
||||||
|
commands_path: str
|
||||||
|
|
||||||
|
logger_config: dict
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
min_any_str_length = 1
|
||||||
|
anystr_strip_whitespace = True
|
@ -0,0 +1,11 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenData(BaseModel):
|
||||||
|
username: str
|
||||||
|
expire: int
|
@ -0,0 +1,49 @@
|
|||||||
|
from fastapi import HTTPException, status
|
||||||
|
from pydantic import BaseModel, validator
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
login: str
|
||||||
|
password: str
|
||||||
|
rights: Optional[List[str]] = []
|
||||||
|
|
||||||
|
|
||||||
|
class UserData(UserCreate):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
id: int
|
||||||
|
login: str
|
||||||
|
rights: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class UserRead(User):
|
||||||
|
@validator("rights")
|
||||||
|
def check_permissions(cls, value):
|
||||||
|
check_rights("read", value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class UserWrite(User):
|
||||||
|
@validator("rights")
|
||||||
|
def check_permissions(cls, value):
|
||||||
|
check_rights("write", value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class UserAdmin(User):
|
||||||
|
@validator("rights")
|
||||||
|
def check_permissions(cls, value):
|
||||||
|
check_rights("admin", value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def check_rights(right: str, rights_list: List[str]):
|
||||||
|
if right not in rights_list:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Not enough permissions",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
@ -0,0 +1,77 @@
|
|||||||
|
from hashlib import pbkdf2_hmac
|
||||||
|
from random import choices
|
||||||
|
from string import ascii_letters
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from typing import Optional, Union
|
||||||
|
from jose import jwt
|
||||||
|
|
||||||
|
from ..schemas.tokens import TokenData
|
||||||
|
from ..schemas.users import UserData
|
||||||
|
|
||||||
|
from .users import get_user_by_username
|
||||||
|
|
||||||
|
from calculate.utils.files import Process
|
||||||
|
|
||||||
|
|
||||||
|
def make_secret_key():
|
||||||
|
openssl_process = Process("/usr/bin/openssl", "rand", "-hex", "32")
|
||||||
|
secret_key = openssl_process.read()
|
||||||
|
return secret_key.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# SECRET_KEY =\
|
||||||
|
# "efe90242c1c221b20fc718edc3aa5da4f78147eb2f4e81e809e945bbcdf0710e"
|
||||||
|
SECRET_KEY = make_secret_key()
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
|
||||||
|
|
||||||
|
async def auth_user(username: str, password: str) -> Union[UserData, bool]:
|
||||||
|
user_row = await get_user_by_username(username)
|
||||||
|
if not user_row:
|
||||||
|
return False
|
||||||
|
user = UserData(**user_row)
|
||||||
|
if not validate_password(password, user.password):
|
||||||
|
return False
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def decode_jwt(token: str) -> Union[TokenData, None]:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
expire = payload.get("exp")
|
||||||
|
if username is None:
|
||||||
|
return None
|
||||||
|
return TokenData(username=username,
|
||||||
|
expire=expire)
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: dict,
|
||||||
|
expires_delta: Optional[timedelta] = None):
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def get_salt(length=12):
|
||||||
|
"""Метод для получения случайной строки символов используемой как соль
|
||||||
|
при кэшировании."""
|
||||||
|
return "".join(choices(ascii_letters, k=length))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(password: str, hashed_password: str) -> bool:
|
||||||
|
salt, db_hash = hashed_password.split("$")
|
||||||
|
hashed = hash_password(password, salt)
|
||||||
|
return hashed == db_hash
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str, salt: str) -> str:
|
||||||
|
if salt is None:
|
||||||
|
salt = get_salt()
|
||||||
|
enc = pbkdf2_hmac("sha256", password.encode(), salt.encode(), 100_000)
|
||||||
|
return enc.hex()
|
@ -0,0 +1,50 @@
|
|||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
|
||||||
|
from jose import JWTError
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from .auth import decode_jwt
|
||||||
|
from .users import get_user_by_username
|
||||||
|
|
||||||
|
from ..schemas.users import UserRead, UserWrite, UserAdmin
|
||||||
|
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
token_data = decode_jwt(token)
|
||||||
|
if token_data is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
except (JWTError, ValidationError):
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
user = await get_user_by_username(token_data.username)
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def make_right_checkers():
|
||||||
|
rights_schemas = {"read": UserRead, "write": UserWrite, "admin": UserAdmin}
|
||||||
|
dependencies = {}
|
||||||
|
for right, schema in rights_schemas.items():
|
||||||
|
async def depend_function(token: str = Depends(oauth2_scheme)):
|
||||||
|
user = await get_current_user(token=token)
|
||||||
|
return schema(**user)
|
||||||
|
|
||||||
|
dependencies[right] = depend_function
|
||||||
|
return dependencies
|
||||||
|
|
||||||
|
|
||||||
|
right_checkers = make_right_checkers()
|
@ -1,5 +1,13 @@
|
|||||||
|
python-ldap
|
||||||
|
requests
|
||||||
|
uvicorn
|
||||||
|
fastapi
|
||||||
pytest
|
pytest
|
||||||
jinja2
|
jinja2
|
||||||
xattr
|
xattr
|
||||||
lxml
|
lxml
|
||||||
mock
|
mock
|
||||||
|
python-jose
|
||||||
|
python-multipart
|
||||||
|
sqlalchemy
|
||||||
|
databases[sqlite]
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from calculate.logging import dictLogConfig
|
||||||
|
|
||||||
|
|
||||||
|
testfiles_path = os.path.join(os.getcwd(), 'tests/server/testfiles')
|
||||||
|
|
||||||
|
|
||||||
|
config = {"socket_path": "./input.sock",
|
||||||
|
"variables_path": 'tests/server/testfiles/variables',
|
||||||
|
"commands_path": 'tests/server/testfiles/commands',
|
||||||
|
"logger_config": dictLogConfig}
|
Loading…
Reference in new issue