authorization: use SQL tables to store user info
Add a UserInfo class which provides type annotations for each member. This class easily converts to a dict or tuple, simplifying conversion for usage in SQL statements. Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
3f0d20ed8c
commit
eddf47e4a3
|
@ -10,8 +10,9 @@ import logging
|
|||
import copy
|
||||
import re
|
||||
import inspect
|
||||
import dataclasses
|
||||
import time
|
||||
from enum import Enum, Flag, auto
|
||||
from dataclasses import dataclass
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from .utils import ServerError, Sentinel
|
||||
from .utils import json_wrapper as jsonw
|
||||
|
@ -177,7 +178,24 @@ class RenderableTemplate(metaclass=ABCMeta):
|
|||
async def render_async(self, context: Dict[str, Any] = {}) -> str:
|
||||
...
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclasses.dataclass
|
||||
class UserInfo:
|
||||
username: str
|
||||
password: str
|
||||
created_on: float = dataclasses.field(default_factory=time.time)
|
||||
salt: str = ""
|
||||
source: str = "moonraker"
|
||||
jwt_secret: Optional[str] = None
|
||||
jwk_id: Optional[str] = None
|
||||
groups: List[str] = dataclasses.field(default_factory=lambda: ["admin"])
|
||||
|
||||
def as_tuple(self) -> Tuple[Any, ...]:
|
||||
return dataclasses.astuple(self)
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
return dataclasses.asdict(self)
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class APIDefinition:
|
||||
endpoint: str
|
||||
http_path: str
|
||||
|
|
|
@ -20,7 +20,7 @@ import logging
|
|||
from tornado.web import HTTPError
|
||||
from libnacl.sign import Signer, Verifier
|
||||
from ..utils import json_wrapper as jsonw
|
||||
from ..common import RequestType, TransportType
|
||||
from ..common import RequestType, TransportType, SqlTableDefinition, UserInfo
|
||||
|
||||
# Annotation imports
|
||||
from typing import (
|
||||
|
@ -39,6 +39,7 @@ if TYPE_CHECKING:
|
|||
from .websockets import WebsocketManager
|
||||
from tornado.httputil import HTTPServerRequest
|
||||
from .database import MoonrakerDatabase as DBComp
|
||||
from .database import DBProviderWrapper
|
||||
from .ldap import MoonrakerLDAP
|
||||
IPAddr = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
||||
IPNetwork = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
|
||||
|
@ -60,6 +61,7 @@ TRUSTED_CONNECTION_TIMEOUT = 3600
|
|||
FQDN_CACHE_TIMEOUT = 84000
|
||||
PRUNE_CHECK_TIME = 300.
|
||||
|
||||
USER_TABLE = "authorized_users"
|
||||
AUTH_SOURCES = ["moonraker", "ldap"]
|
||||
HASH_ITER = 100000
|
||||
API_USER = "_API_KEY_USER_"
|
||||
|
@ -71,6 +73,47 @@ JWT_HEADER = {
|
|||
'typ': "JWT"
|
||||
}
|
||||
|
||||
class UserSqlDefinition(SqlTableDefinition):
|
||||
name = USER_TABLE
|
||||
prototype = (
|
||||
f"""
|
||||
{USER_TABLE} (
|
||||
username TEXT PRIMARY KEY NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created_on REAL NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
jwt_secret TEXT,
|
||||
jwk_id TEXT,
|
||||
groups pyjson
|
||||
)
|
||||
"""
|
||||
)
|
||||
version = 1
|
||||
|
||||
def migrate(self, last_version: int, db_provider: DBProviderWrapper) -> None:
|
||||
if last_version == 0:
|
||||
users: Dict[str, Dict[str, Any]]
|
||||
users = db_provider.get_namespace("authorized_users")
|
||||
api_user = users.pop(API_USER, {})
|
||||
user_vals: List[Tuple[Any, ...]] = [
|
||||
UserInfo(
|
||||
username=API_USER,
|
||||
password=api_user.get("api_key", uuid.uuid4().hex),
|
||||
created_on=api_user.get("created_on", time.time())
|
||||
).as_tuple()
|
||||
]
|
||||
for user in users.values():
|
||||
user_vals.append(UserInfo(**user).as_tuple())
|
||||
placeholders = ",".join("?" * len(user_vals[0]))
|
||||
conn = db_provider.connection
|
||||
with conn:
|
||||
conn.executemany(
|
||||
f"INSERT OR IGNORE INTO {USER_TABLE} VALUES({placeholders})",
|
||||
user_vals
|
||||
)
|
||||
db_provider.wipe_local_namespace("authorized_users")
|
||||
|
||||
class Authorization:
|
||||
def __init__(self, config: ConfigHelper) -> None:
|
||||
self.server = config.get_server()
|
||||
|
@ -97,62 +140,13 @@ class Authorization:
|
|||
" however [ldap] section failed to load or not configured"
|
||||
)
|
||||
database: DBComp = self.server.lookup_component('database')
|
||||
database.register_local_namespace('authorized_users', forbidden=True)
|
||||
self.user_db = database.wrap_namespace('authorized_users')
|
||||
self.users: Dict[str, Dict[str, Any]] = self.user_db.as_dict()
|
||||
api_user: Optional[Dict[str, Any]] = self.users.get(API_USER, None)
|
||||
if api_user is None:
|
||||
self.user_table = database.register_table(UserSqlDefinition())
|
||||
self.users: Dict[str, UserInfo] = {}
|
||||
self.api_key = uuid.uuid4().hex
|
||||
self.users[API_USER] = {
|
||||
'username': API_USER,
|
||||
'api_key': self.api_key,
|
||||
'created_on': time.time()
|
||||
}
|
||||
else:
|
||||
self.api_key = api_user['api_key']
|
||||
hi = self.server.get_host_info()
|
||||
self.issuer = f"http://{hi['hostname']}:{hi['port']}"
|
||||
self.public_jwks: Dict[str, Dict[str, Any]] = {}
|
||||
for username, user_info in list(self.users.items()):
|
||||
if username == API_USER:
|
||||
# Validate the API User
|
||||
for item in ["username", "api_key", "created_on"]:
|
||||
if item not in user_info:
|
||||
self.users[API_USER] = {
|
||||
'username': API_USER,
|
||||
'api_key': self.api_key,
|
||||
'created_on': time.time()
|
||||
}
|
||||
break
|
||||
continue
|
||||
else:
|
||||
# validate created users
|
||||
valid = True
|
||||
for item in ["username", "password", "salt", "created_on"]:
|
||||
if item not in user_info:
|
||||
logging.info(
|
||||
f"Authorization: User {username} does not "
|
||||
f"contain field {item}, removing")
|
||||
del self.users[username]
|
||||
valid = False
|
||||
break
|
||||
if not valid:
|
||||
continue
|
||||
# generate jwks for valid users
|
||||
if 'jwt_secret' in user_info:
|
||||
try:
|
||||
priv_key = self._load_private_key(user_info['jwt_secret'])
|
||||
jwk_id = user_info['jwk_id']
|
||||
except (self.server.error, KeyError):
|
||||
logging.info("Invalid key found for user, removing")
|
||||
user_info.pop('jwt_secret', None)
|
||||
user_info.pop('jwk_id', None)
|
||||
self.users[username] = user_info
|
||||
continue
|
||||
self.public_jwks[jwk_id] = self._generate_public_jwk(priv_key)
|
||||
# sync user changes to the database
|
||||
self.user_db.sync(self.users)
|
||||
self.trusted_users: Dict[IPAddr, Any] = {}
|
||||
self.trusted_users: Dict[IPAddr, Dict[str, Any]] = {}
|
||||
self.oneshot_tokens: Dict[str, OneshotToken] = {}
|
||||
|
||||
# Get allowed cors domains
|
||||
|
@ -276,17 +270,68 @@ class Authorization:
|
|||
"authorization:user_logged_out", event_type="logout"
|
||||
)
|
||||
|
||||
def _sync_user(self, username: str) -> None:
|
||||
self.user_db[username] = self.users[username]
|
||||
|
||||
async def component_init(self) -> None:
|
||||
# Populate users from database
|
||||
cursor = await self.user_table.execute(f"SELECT * FROM {USER_TABLE}")
|
||||
self.users = {row[0]: UserInfo(**dict(row)) for row in await cursor.fetchall()}
|
||||
need_sync = self._initialize_users()
|
||||
if need_sync:
|
||||
await self._sync_user_table()
|
||||
self.prune_timer.start(delay=PRUNE_CHECK_TIME)
|
||||
|
||||
async def _sync_user(self, username: str) -> None:
|
||||
user = self.users[username]
|
||||
vals = user.as_tuple()
|
||||
placeholders = ",".join("?" * len(vals))
|
||||
async with self.user_table as tx:
|
||||
await tx.execute(
|
||||
f"REPLACE INTO {USER_TABLE} VALUES({placeholders})", vals
|
||||
)
|
||||
|
||||
async def _sync_user_table(self) -> None:
|
||||
async with self.user_table as tx:
|
||||
await tx.execute(f"DELETE FROM {USER_TABLE}")
|
||||
user_vals: List[Tuple[Any, ...]]
|
||||
user_vals = [user.as_tuple() for user in self.users.values()]
|
||||
if not user_vals:
|
||||
return
|
||||
placeholders = ",".join("?" * len(user_vals[0]))
|
||||
await tx.executemany(
|
||||
f"INSERT INTO {USER_TABLE} VALUES({placeholders})", user_vals
|
||||
)
|
||||
|
||||
def _initialize_users(self) -> bool:
|
||||
need_sync = False
|
||||
api_user: Optional[UserInfo] = self.users.get(API_USER, None)
|
||||
if api_user is None:
|
||||
need_sync = True
|
||||
self.users[API_USER] = UserInfo(username=API_USER, password=self.api_key)
|
||||
else:
|
||||
self.api_key = api_user.password
|
||||
for username, user_info in list(self.users.items()):
|
||||
if username == API_USER:
|
||||
continue
|
||||
# generate jwks for valid users
|
||||
if user_info.jwt_secret is not None:
|
||||
try:
|
||||
priv_key = self._load_private_key(user_info.jwt_secret)
|
||||
jwk_id = user_info.jwk_id
|
||||
assert jwk_id is not None
|
||||
except (self.server.error, KeyError, AssertionError):
|
||||
logging.info("Invalid jwk found for user, removing")
|
||||
user_info.jwt_secret = None
|
||||
user_info.jwk_id = None
|
||||
self.users[username] = user_info
|
||||
need_sync = True
|
||||
continue
|
||||
self.public_jwks[jwk_id] = self._generate_public_jwk(priv_key)
|
||||
return need_sync
|
||||
|
||||
async def _handle_apikey_request(self, web_request: WebRequest) -> str:
|
||||
if web_request.get_request_type() == RequestType.POST:
|
||||
self.api_key = uuid.uuid4().hex
|
||||
self.users[API_USER]['api_key'] = self.api_key
|
||||
self._sync_user(API_USER)
|
||||
self.users[API_USER].password = self.api_key
|
||||
await self._sync_user(API_USER)
|
||||
return self.api_key
|
||||
|
||||
async def _handle_oneshot_request(self, web_request: WebRequest) -> str:
|
||||
|
@ -322,10 +367,12 @@ class Authorization:
|
|||
if username in RESERVED_USERS:
|
||||
raise self.server.error(
|
||||
f"Invalid log out request for user {username}")
|
||||
self.users[username].pop("jwt_secret", None)
|
||||
jwk_id: str = self.users[username].pop("jwk_id", None)
|
||||
self._sync_user(username)
|
||||
jwk_id: Optional[str] = self.users[username].jwk_id
|
||||
self.users[username].jwt_secret = None
|
||||
self.users[username].jwk_id = None
|
||||
if jwk_id is not None:
|
||||
self.public_jwks.pop(jwk_id, None)
|
||||
await self._sync_user(username)
|
||||
eventloop = self.server.get_event_loop()
|
||||
eventloop.delay_callback(
|
||||
.005, self.server.send_event, "authorization:user_logged_out",
|
||||
|
@ -363,16 +410,16 @@ class Authorization:
|
|||
user_info = self.decode_jwt(refresh_token, token_type="refresh")
|
||||
except Exception:
|
||||
raise self.server.error("Invalid Refresh Token", 401)
|
||||
username: str = user_info['username']
|
||||
if 'jwt_secret' not in user_info or "jwk_id" not in user_info:
|
||||
username: str = user_info.username
|
||||
if user_info.jwt_secret is None or user_info.jwk_id is None:
|
||||
raise self.server.error("User not logged in", 401)
|
||||
private_key = self._load_private_key(user_info['jwt_secret'])
|
||||
jwk_id: str = user_info['jwk_id']
|
||||
private_key = self._load_private_key(user_info.jwt_secret)
|
||||
jwk_id: str = user_info.jwk_id
|
||||
token = self._generate_jwt(username, jwk_id, private_key)
|
||||
return {
|
||||
'username': username,
|
||||
'token': token,
|
||||
'source': user_info.get("source", "moonraker"),
|
||||
'source': user_info.source,
|
||||
'action': 'user_jwt_refresh'
|
||||
}
|
||||
|
||||
|
@ -399,7 +446,7 @@ class Authorization:
|
|||
return await self._login_jwt_user(web_request, create=True)
|
||||
elif req_type == RequestType.DELETE:
|
||||
# Delete User
|
||||
return self._delete_jwt_user(web_request)
|
||||
return await self._delete_jwt_user(web_request)
|
||||
raise self.server.error("Invalid Request Method")
|
||||
|
||||
async def _handle_list_request(self,
|
||||
|
@ -407,12 +454,12 @@ class Authorization:
|
|||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
user_list = []
|
||||
for user in self.users.values():
|
||||
if user['username'] == API_USER:
|
||||
if user.username == API_USER:
|
||||
continue
|
||||
user_list.append({
|
||||
'username': user['username'],
|
||||
'source': user.get("source", "moonraker"),
|
||||
'created_on': user['created_on']
|
||||
'username': user.username,
|
||||
'source': user.source,
|
||||
'created_on': user.created_on
|
||||
})
|
||||
return {
|
||||
'users': user_list
|
||||
|
@ -440,8 +487,8 @@ class Authorization:
|
|||
raise self.server.error("Invalid Password")
|
||||
new_hashed_pass = hashlib.pbkdf2_hmac(
|
||||
'sha256', new_pass.encode(), salt, HASH_ITER).hex()
|
||||
self.users[username]['password'] = new_hashed_pass
|
||||
self._sync_user(username)
|
||||
self.users[username].password = new_hashed_pass
|
||||
await self._sync_user(username)
|
||||
return {
|
||||
'username': username,
|
||||
'action': "user_password_reset"
|
||||
|
@ -457,7 +504,7 @@ class Authorization:
|
|||
).lower()
|
||||
if source not in AUTH_SOURCES:
|
||||
raise self.server.error(f"Invalid 'source': {source}")
|
||||
user_info: Dict[str, Any]
|
||||
user_info: UserInfo
|
||||
if username in RESERVED_USERS:
|
||||
raise self.server.error(
|
||||
f"Invalid Request for user {username}")
|
||||
|
@ -477,15 +524,14 @@ class Authorization:
|
|||
salt = secrets.token_bytes(32)
|
||||
hashed_pass = hashlib.pbkdf2_hmac(
|
||||
'sha256', password.encode(), salt, HASH_ITER).hex()
|
||||
user_info = {
|
||||
'username': username,
|
||||
'password': hashed_pass,
|
||||
'salt': salt.hex(),
|
||||
'source': source,
|
||||
'created_on': time.time()
|
||||
}
|
||||
user_info = UserInfo(
|
||||
username=username,
|
||||
password=hashed_pass,
|
||||
salt=salt.hex(),
|
||||
source=source,
|
||||
)
|
||||
self.users[username] = user_info
|
||||
self._sync_user(username)
|
||||
await self._sync_user(username)
|
||||
action = "user_created"
|
||||
if source == "ldap":
|
||||
# Dont notify user created
|
||||
|
@ -495,30 +541,32 @@ class Authorization:
|
|||
if username not in self.users:
|
||||
raise self.server.error(f"Unregistered User: {username}")
|
||||
user_info = self.users[username]
|
||||
auth_src = user_info.get("source", "moonraker")
|
||||
auth_src = user_info.source
|
||||
if auth_src != source:
|
||||
raise self.server.error(
|
||||
f"Moonraker cannot authenticate user '{username}', must "
|
||||
f"specify source '{auth_src}'", 401
|
||||
)
|
||||
salt = bytes.fromhex(user_info['salt'])
|
||||
salt = bytes.fromhex(user_info.salt)
|
||||
hashed_pass = hashlib.pbkdf2_hmac(
|
||||
'sha256', password.encode(), salt, HASH_ITER).hex()
|
||||
action = "user_logged_in"
|
||||
if hashed_pass != user_info['password']:
|
||||
if hashed_pass != user_info.password:
|
||||
raise self.server.error("Invalid Password")
|
||||
jwt_secret_hex: Optional[str] = user_info.get('jwt_secret', None)
|
||||
jwt_secret_hex: Optional[str] = user_info.jwt_secret
|
||||
if jwt_secret_hex is None:
|
||||
private_key = Signer()
|
||||
jwk_id = base64url_encode(secrets.token_bytes()).decode()
|
||||
user_info['jwt_secret'] = private_key.hex_seed().decode()
|
||||
user_info['jwk_id'] = jwk_id
|
||||
user_info.jwt_secret = private_key.hex_seed().decode()
|
||||
user_info.jwk_id = jwk_id
|
||||
self.users[username] = user_info
|
||||
self._sync_user(username)
|
||||
await self._sync_user(username)
|
||||
self.public_jwks[jwk_id] = self._generate_public_jwk(private_key)
|
||||
else:
|
||||
private_key = self._load_private_key(jwt_secret_hex)
|
||||
jwk_id = user_info['jwk_id']
|
||||
if user_info.jwk_id is None:
|
||||
user_info.jwk_id = base64url_encode(secrets.token_bytes()).decode()
|
||||
jwk_id = user_info.jwk_id
|
||||
token = self._generate_jwt(username, jwk_id, private_key)
|
||||
refresh_token = self._generate_jwt(
|
||||
username, jwk_id, private_key, token_type="refresh",
|
||||
|
@ -531,16 +579,16 @@ class Authorization:
|
|||
"authorization:user_created",
|
||||
{'username': username})
|
||||
elif conn is not None:
|
||||
conn.user_info = user_info
|
||||
conn.user_info = user_info.as_dict()
|
||||
return {
|
||||
'username': username,
|
||||
'token': token,
|
||||
'source': user_info.get("source", "moonraker"),
|
||||
'source': user_info.source,
|
||||
'refresh_token': refresh_token,
|
||||
'action': action
|
||||
}
|
||||
|
||||
def _delete_jwt_user(self, web_request: WebRequest) -> Dict[str, str]:
|
||||
async def _delete_jwt_user(self, web_request: WebRequest) -> Dict[str, str]:
|
||||
username: str = web_request.get_str('username')
|
||||
current_user = web_request.get_current_user()
|
||||
if current_user is not None:
|
||||
|
@ -551,13 +599,16 @@ class Authorization:
|
|||
if username in RESERVED_USERS:
|
||||
raise self.server.error(
|
||||
f"Invalid Request for reserved user {username}")
|
||||
user_info: Optional[Dict[str, Any]] = self.users.get(username)
|
||||
user_info: Optional[UserInfo] = self.users.get(username)
|
||||
if user_info is None:
|
||||
raise self.server.error(f"No registered user: {username}")
|
||||
if 'jwk_id' in user_info:
|
||||
self.public_jwks.pop(user_info['jwk_id'], None)
|
||||
if user_info.jwk_id is not None:
|
||||
self.public_jwks.pop(user_info.jwk_id, None)
|
||||
del self.users[username]
|
||||
del self.user_db[username]
|
||||
async with self.user_table as tx:
|
||||
await tx.execute(
|
||||
f"DELETE FROM {USER_TABLE} WHERE username = ?", (username,)
|
||||
)
|
||||
event_loop = self.server.get_event_loop()
|
||||
event_loop.delay_callback(
|
||||
.005, self.server.send_event,
|
||||
|
@ -595,7 +646,7 @@ class Authorization:
|
|||
|
||||
def decode_jwt(
|
||||
self, token: str, token_type: str = "access", check_exp: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
) -> UserInfo:
|
||||
message, sig = token.rsplit('.', maxsplit=1)
|
||||
enc_header, enc_payload = message.split('.')
|
||||
header: Dict[str, Any] = jsonw.loads(base64url_decode(enc_header))
|
||||
|
@ -626,7 +677,7 @@ class Authorization:
|
|||
raise self.server.error("JWT Expired", 401)
|
||||
|
||||
# get user
|
||||
user_info: Optional[Dict[str, Any]] = self.users.get(
|
||||
user_info: Optional[UserInfo] = self.users.get(
|
||||
payload.get('username', ""), None)
|
||||
if user_info is None:
|
||||
raise self.server.error("Unknown user", 401)
|
||||
|
@ -641,13 +692,13 @@ class Authorization:
|
|||
raise self.server.error(
|
||||
f"Failed to decode JWT: {e}", 401
|
||||
) from e
|
||||
return user_info
|
||||
return user_info.as_dict()
|
||||
|
||||
def validate_api_key(self, api_key: str) -> Dict[str, Any]:
|
||||
if not self.enable_api_key:
|
||||
raise self.server.error("API Key authentication is disabled", 401)
|
||||
if api_key and api_key == self.api_key:
|
||||
return self.users[API_USER]
|
||||
return self.users[API_USER].as_dict()
|
||||
raise self.server.error("Invalid API Key", 401)
|
||||
|
||||
def _load_private_key(self, secret: str) -> Signer:
|
||||
|
@ -709,7 +760,7 @@ class Authorization:
|
|||
|
||||
def _check_json_web_token(
|
||||
self, request: HTTPServerRequest, required: bool = True
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
) -> Optional[UserInfo]:
|
||||
auth_token: Optional[str] = request.headers.get("Authorization")
|
||||
if auth_token is None:
|
||||
auth_token = request.headers.get("X-Access-Token")
|
||||
|
@ -757,23 +808,21 @@ class Authorization:
|
|||
|
||||
async def _check_trusted_connection(
|
||||
self, ip: Optional[IPAddr]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
) -> Optional[UserInfo]:
|
||||
if ip is not None:
|
||||
curtime = time.time()
|
||||
exp_time = curtime + TRUSTED_CONNECTION_TIMEOUT
|
||||
if ip in self.trusted_users:
|
||||
self.trusted_users[ip]['expires_at'] = exp_time
|
||||
return self.trusted_users[ip]
|
||||
self.trusted_users[ip]["expires_at"] = exp_time
|
||||
return self.trusted_users[ip]["user"]
|
||||
elif await self._check_authorized_ip(ip):
|
||||
logging.info(
|
||||
f"Trusted Connection Detected, IP: {ip}")
|
||||
self.trusted_users[ip] = {
|
||||
'username': TRUSTED_USER,
|
||||
'password': None,
|
||||
'created_on': curtime,
|
||||
'expires_at': exp_time
|
||||
"user": UserInfo(TRUSTED_USER, "", curtime),
|
||||
"expires_at": exp_time
|
||||
}
|
||||
return self.trusted_users[ip]
|
||||
return self.trusted_users[ip]["user"]
|
||||
return None
|
||||
|
||||
def _check_oneshot_token(self,
|
||||
|
@ -805,7 +854,7 @@ class Authorization:
|
|||
# Check JSON Web Token
|
||||
jwt_user = self._check_json_web_token(request, auth_required)
|
||||
if jwt_user is not None:
|
||||
return jwt_user
|
||||
return jwt_user.as_dict()
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(request.remote_ip) # type: ignore
|
||||
|
@ -825,7 +874,7 @@ class Authorization:
|
|||
if self.enable_api_key:
|
||||
key: Optional[str] = request.headers.get("X-Api-Key")
|
||||
if key and key == self.api_key:
|
||||
return self.users[API_USER]
|
||||
return self.users[API_USER].as_dict()
|
||||
|
||||
# If the force_logins option is enabled and at least one user is created
|
||||
# then trusted user authentication is disabled
|
||||
|
@ -837,8 +886,10 @@ class Authorization:
|
|||
# Check if IP is trusted. If this endpoint doesn't require authentication
|
||||
# then it is acceptable to return None
|
||||
trusted_user = await self._check_trusted_connection(ip)
|
||||
if trusted_user is not None or not auth_required:
|
||||
return trusted_user
|
||||
if trusted_user is not None:
|
||||
return trusted_user.as_dict()
|
||||
if not auth_required:
|
||||
return None
|
||||
|
||||
raise HTTPError(401, "Unauthorized")
|
||||
|
||||
|
|
Loading…
Reference in New Issue