父節點
a2e6088216
當前提交
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
|
||||
jinja2
|
||||
xattr
|
||||
lxml
|
||||
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}
|
載入中…
新增問題並參考