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 copy
|
||||||
import re
|
import re
|
||||||
import inspect
|
import inspect
|
||||||
|
import dataclasses
|
||||||
|
import time
|
||||||
from enum import Enum, Flag, auto
|
from enum import Enum, Flag, auto
|
||||||
from dataclasses import dataclass
|
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from .utils import ServerError, Sentinel
|
from .utils import ServerError, Sentinel
|
||||||
from .utils import json_wrapper as jsonw
|
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:
|
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:
|
class APIDefinition:
|
||||||
endpoint: str
|
endpoint: str
|
||||||
http_path: str
|
http_path: str
|
||||||
|
|
|
@ -20,7 +20,7 @@ import logging
|
||||||
from tornado.web import HTTPError
|
from tornado.web import HTTPError
|
||||||
from libnacl.sign import Signer, Verifier
|
from libnacl.sign import Signer, Verifier
|
||||||
from ..utils import json_wrapper as jsonw
|
from ..utils import json_wrapper as jsonw
|
||||||
from ..common import RequestType, TransportType
|
from ..common import RequestType, TransportType, SqlTableDefinition, UserInfo
|
||||||
|
|
||||||
# Annotation imports
|
# Annotation imports
|
||||||
from typing import (
|
from typing import (
|
||||||
|
@ -39,6 +39,7 @@ if TYPE_CHECKING:
|
||||||
from .websockets import WebsocketManager
|
from .websockets import WebsocketManager
|
||||||
from tornado.httputil import HTTPServerRequest
|
from tornado.httputil import HTTPServerRequest
|
||||||
from .database import MoonrakerDatabase as DBComp
|
from .database import MoonrakerDatabase as DBComp
|
||||||
|
from .database import DBProviderWrapper
|
||||||
from .ldap import MoonrakerLDAP
|
from .ldap import MoonrakerLDAP
|
||||||
IPAddr = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
IPAddr = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
||||||
IPNetwork = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
|
IPNetwork = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
|
||||||
|
@ -60,6 +61,7 @@ TRUSTED_CONNECTION_TIMEOUT = 3600
|
||||||
FQDN_CACHE_TIMEOUT = 84000
|
FQDN_CACHE_TIMEOUT = 84000
|
||||||
PRUNE_CHECK_TIME = 300.
|
PRUNE_CHECK_TIME = 300.
|
||||||
|
|
||||||
|
USER_TABLE = "authorized_users"
|
||||||
AUTH_SOURCES = ["moonraker", "ldap"]
|
AUTH_SOURCES = ["moonraker", "ldap"]
|
||||||
HASH_ITER = 100000
|
HASH_ITER = 100000
|
||||||
API_USER = "_API_KEY_USER_"
|
API_USER = "_API_KEY_USER_"
|
||||||
|
@ -71,6 +73,47 @@ JWT_HEADER = {
|
||||||
'typ': "JWT"
|
'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:
|
class Authorization:
|
||||||
def __init__(self, config: ConfigHelper) -> None:
|
def __init__(self, config: ConfigHelper) -> None:
|
||||||
self.server = config.get_server()
|
self.server = config.get_server()
|
||||||
|
@ -97,62 +140,13 @@ class Authorization:
|
||||||
" however [ldap] section failed to load or not configured"
|
" however [ldap] section failed to load or not configured"
|
||||||
)
|
)
|
||||||
database: DBComp = self.server.lookup_component('database')
|
database: DBComp = self.server.lookup_component('database')
|
||||||
database.register_local_namespace('authorized_users', forbidden=True)
|
self.user_table = database.register_table(UserSqlDefinition())
|
||||||
self.user_db = database.wrap_namespace('authorized_users')
|
self.users: Dict[str, UserInfo] = {}
|
||||||
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.api_key = uuid.uuid4().hex
|
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()
|
hi = self.server.get_host_info()
|
||||||
self.issuer = f"http://{hi['hostname']}:{hi['port']}"
|
self.issuer = f"http://{hi['hostname']}:{hi['port']}"
|
||||||
self.public_jwks: Dict[str, Dict[str, Any]] = {}
|
self.public_jwks: Dict[str, Dict[str, Any]] = {}
|
||||||
for username, user_info in list(self.users.items()):
|
self.trusted_users: Dict[IPAddr, Dict[str, Any]] = {}
|
||||||
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.oneshot_tokens: Dict[str, OneshotToken] = {}
|
self.oneshot_tokens: Dict[str, OneshotToken] = {}
|
||||||
|
|
||||||
# Get allowed cors domains
|
# Get allowed cors domains
|
||||||
|
@ -276,17 +270,68 @@ class Authorization:
|
||||||
"authorization:user_logged_out", event_type="logout"
|
"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:
|
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)
|
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:
|
async def _handle_apikey_request(self, web_request: WebRequest) -> str:
|
||||||
if web_request.get_request_type() == RequestType.POST:
|
if web_request.get_request_type() == RequestType.POST:
|
||||||
self.api_key = uuid.uuid4().hex
|
self.api_key = uuid.uuid4().hex
|
||||||
self.users[API_USER]['api_key'] = self.api_key
|
self.users[API_USER].password = self.api_key
|
||||||
self._sync_user(API_USER)
|
await self._sync_user(API_USER)
|
||||||
return self.api_key
|
return self.api_key
|
||||||
|
|
||||||
async def _handle_oneshot_request(self, web_request: WebRequest) -> str:
|
async def _handle_oneshot_request(self, web_request: WebRequest) -> str:
|
||||||
|
@ -322,10 +367,12 @@ class Authorization:
|
||||||
if username in RESERVED_USERS:
|
if username in RESERVED_USERS:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Invalid log out request for user {username}")
|
f"Invalid log out request for user {username}")
|
||||||
self.users[username].pop("jwt_secret", None)
|
jwk_id: Optional[str] = self.users[username].jwk_id
|
||||||
jwk_id: str = self.users[username].pop("jwk_id", None)
|
self.users[username].jwt_secret = None
|
||||||
self._sync_user(username)
|
self.users[username].jwk_id = None
|
||||||
|
if jwk_id is not None:
|
||||||
self.public_jwks.pop(jwk_id, None)
|
self.public_jwks.pop(jwk_id, None)
|
||||||
|
await self._sync_user(username)
|
||||||
eventloop = self.server.get_event_loop()
|
eventloop = self.server.get_event_loop()
|
||||||
eventloop.delay_callback(
|
eventloop.delay_callback(
|
||||||
.005, self.server.send_event, "authorization:user_logged_out",
|
.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")
|
user_info = self.decode_jwt(refresh_token, token_type="refresh")
|
||||||
except Exception:
|
except Exception:
|
||||||
raise self.server.error("Invalid Refresh Token", 401)
|
raise self.server.error("Invalid Refresh Token", 401)
|
||||||
username: str = user_info['username']
|
username: str = user_info.username
|
||||||
if 'jwt_secret' not in user_info or "jwk_id" not in user_info:
|
if user_info.jwt_secret is None or user_info.jwk_id is None:
|
||||||
raise self.server.error("User not logged in", 401)
|
raise self.server.error("User not logged in", 401)
|
||||||
private_key = self._load_private_key(user_info['jwt_secret'])
|
private_key = self._load_private_key(user_info.jwt_secret)
|
||||||
jwk_id: str = user_info['jwk_id']
|
jwk_id: str = user_info.jwk_id
|
||||||
token = self._generate_jwt(username, jwk_id, private_key)
|
token = self._generate_jwt(username, jwk_id, private_key)
|
||||||
return {
|
return {
|
||||||
'username': username,
|
'username': username,
|
||||||
'token': token,
|
'token': token,
|
||||||
'source': user_info.get("source", "moonraker"),
|
'source': user_info.source,
|
||||||
'action': 'user_jwt_refresh'
|
'action': 'user_jwt_refresh'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,7 +446,7 @@ class Authorization:
|
||||||
return await self._login_jwt_user(web_request, create=True)
|
return await self._login_jwt_user(web_request, create=True)
|
||||||
elif req_type == RequestType.DELETE:
|
elif req_type == RequestType.DELETE:
|
||||||
# Delete User
|
# Delete User
|
||||||
return self._delete_jwt_user(web_request)
|
return await self._delete_jwt_user(web_request)
|
||||||
raise self.server.error("Invalid Request Method")
|
raise self.server.error("Invalid Request Method")
|
||||||
|
|
||||||
async def _handle_list_request(self,
|
async def _handle_list_request(self,
|
||||||
|
@ -407,12 +454,12 @@ class Authorization:
|
||||||
) -> Dict[str, List[Dict[str, Any]]]:
|
) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
user_list = []
|
user_list = []
|
||||||
for user in self.users.values():
|
for user in self.users.values():
|
||||||
if user['username'] == API_USER:
|
if user.username == API_USER:
|
||||||
continue
|
continue
|
||||||
user_list.append({
|
user_list.append({
|
||||||
'username': user['username'],
|
'username': user.username,
|
||||||
'source': user.get("source", "moonraker"),
|
'source': user.source,
|
||||||
'created_on': user['created_on']
|
'created_on': user.created_on
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
'users': user_list
|
'users': user_list
|
||||||
|
@ -440,8 +487,8 @@ class Authorization:
|
||||||
raise self.server.error("Invalid Password")
|
raise self.server.error("Invalid Password")
|
||||||
new_hashed_pass = hashlib.pbkdf2_hmac(
|
new_hashed_pass = hashlib.pbkdf2_hmac(
|
||||||
'sha256', new_pass.encode(), salt, HASH_ITER).hex()
|
'sha256', new_pass.encode(), salt, HASH_ITER).hex()
|
||||||
self.users[username]['password'] = new_hashed_pass
|
self.users[username].password = new_hashed_pass
|
||||||
self._sync_user(username)
|
await self._sync_user(username)
|
||||||
return {
|
return {
|
||||||
'username': username,
|
'username': username,
|
||||||
'action': "user_password_reset"
|
'action': "user_password_reset"
|
||||||
|
@ -457,7 +504,7 @@ class Authorization:
|
||||||
).lower()
|
).lower()
|
||||||
if source not in AUTH_SOURCES:
|
if source not in AUTH_SOURCES:
|
||||||
raise self.server.error(f"Invalid 'source': {source}")
|
raise self.server.error(f"Invalid 'source': {source}")
|
||||||
user_info: Dict[str, Any]
|
user_info: UserInfo
|
||||||
if username in RESERVED_USERS:
|
if username in RESERVED_USERS:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Invalid Request for user {username}")
|
f"Invalid Request for user {username}")
|
||||||
|
@ -477,15 +524,14 @@ class Authorization:
|
||||||
salt = secrets.token_bytes(32)
|
salt = secrets.token_bytes(32)
|
||||||
hashed_pass = hashlib.pbkdf2_hmac(
|
hashed_pass = hashlib.pbkdf2_hmac(
|
||||||
'sha256', password.encode(), salt, HASH_ITER).hex()
|
'sha256', password.encode(), salt, HASH_ITER).hex()
|
||||||
user_info = {
|
user_info = UserInfo(
|
||||||
'username': username,
|
username=username,
|
||||||
'password': hashed_pass,
|
password=hashed_pass,
|
||||||
'salt': salt.hex(),
|
salt=salt.hex(),
|
||||||
'source': source,
|
source=source,
|
||||||
'created_on': time.time()
|
)
|
||||||
}
|
|
||||||
self.users[username] = user_info
|
self.users[username] = user_info
|
||||||
self._sync_user(username)
|
await self._sync_user(username)
|
||||||
action = "user_created"
|
action = "user_created"
|
||||||
if source == "ldap":
|
if source == "ldap":
|
||||||
# Dont notify user created
|
# Dont notify user created
|
||||||
|
@ -495,30 +541,32 @@ class Authorization:
|
||||||
if username not in self.users:
|
if username not in self.users:
|
||||||
raise self.server.error(f"Unregistered User: {username}")
|
raise self.server.error(f"Unregistered User: {username}")
|
||||||
user_info = self.users[username]
|
user_info = self.users[username]
|
||||||
auth_src = user_info.get("source", "moonraker")
|
auth_src = user_info.source
|
||||||
if auth_src != source:
|
if auth_src != source:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Moonraker cannot authenticate user '{username}', must "
|
f"Moonraker cannot authenticate user '{username}', must "
|
||||||
f"specify source '{auth_src}'", 401
|
f"specify source '{auth_src}'", 401
|
||||||
)
|
)
|
||||||
salt = bytes.fromhex(user_info['salt'])
|
salt = bytes.fromhex(user_info.salt)
|
||||||
hashed_pass = hashlib.pbkdf2_hmac(
|
hashed_pass = hashlib.pbkdf2_hmac(
|
||||||
'sha256', password.encode(), salt, HASH_ITER).hex()
|
'sha256', password.encode(), salt, HASH_ITER).hex()
|
||||||
action = "user_logged_in"
|
action = "user_logged_in"
|
||||||
if hashed_pass != user_info['password']:
|
if hashed_pass != user_info.password:
|
||||||
raise self.server.error("Invalid 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:
|
if jwt_secret_hex is None:
|
||||||
private_key = Signer()
|
private_key = Signer()
|
||||||
jwk_id = base64url_encode(secrets.token_bytes()).decode()
|
jwk_id = base64url_encode(secrets.token_bytes()).decode()
|
||||||
user_info['jwt_secret'] = private_key.hex_seed().decode()
|
user_info.jwt_secret = private_key.hex_seed().decode()
|
||||||
user_info['jwk_id'] = jwk_id
|
user_info.jwk_id = jwk_id
|
||||||
self.users[username] = user_info
|
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)
|
self.public_jwks[jwk_id] = self._generate_public_jwk(private_key)
|
||||||
else:
|
else:
|
||||||
private_key = self._load_private_key(jwt_secret_hex)
|
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)
|
token = self._generate_jwt(username, jwk_id, private_key)
|
||||||
refresh_token = self._generate_jwt(
|
refresh_token = self._generate_jwt(
|
||||||
username, jwk_id, private_key, token_type="refresh",
|
username, jwk_id, private_key, token_type="refresh",
|
||||||
|
@ -531,16 +579,16 @@ class Authorization:
|
||||||
"authorization:user_created",
|
"authorization:user_created",
|
||||||
{'username': username})
|
{'username': username})
|
||||||
elif conn is not None:
|
elif conn is not None:
|
||||||
conn.user_info = user_info
|
conn.user_info = user_info.as_dict()
|
||||||
return {
|
return {
|
||||||
'username': username,
|
'username': username,
|
||||||
'token': token,
|
'token': token,
|
||||||
'source': user_info.get("source", "moonraker"),
|
'source': user_info.source,
|
||||||
'refresh_token': refresh_token,
|
'refresh_token': refresh_token,
|
||||||
'action': action
|
'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')
|
username: str = web_request.get_str('username')
|
||||||
current_user = web_request.get_current_user()
|
current_user = web_request.get_current_user()
|
||||||
if current_user is not None:
|
if current_user is not None:
|
||||||
|
@ -551,13 +599,16 @@ class Authorization:
|
||||||
if username in RESERVED_USERS:
|
if username in RESERVED_USERS:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Invalid Request for reserved user {username}")
|
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:
|
if user_info is None:
|
||||||
raise self.server.error(f"No registered user: {username}")
|
raise self.server.error(f"No registered user: {username}")
|
||||||
if 'jwk_id' in user_info:
|
if user_info.jwk_id is not None:
|
||||||
self.public_jwks.pop(user_info['jwk_id'], None)
|
self.public_jwks.pop(user_info.jwk_id, None)
|
||||||
del self.users[username]
|
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 = self.server.get_event_loop()
|
||||||
event_loop.delay_callback(
|
event_loop.delay_callback(
|
||||||
.005, self.server.send_event,
|
.005, self.server.send_event,
|
||||||
|
@ -595,7 +646,7 @@ class Authorization:
|
||||||
|
|
||||||
def decode_jwt(
|
def decode_jwt(
|
||||||
self, token: str, token_type: str = "access", check_exp: bool = True
|
self, token: str, token_type: str = "access", check_exp: bool = True
|
||||||
) -> Dict[str, Any]:
|
) -> UserInfo:
|
||||||
message, sig = token.rsplit('.', maxsplit=1)
|
message, sig = token.rsplit('.', maxsplit=1)
|
||||||
enc_header, enc_payload = message.split('.')
|
enc_header, enc_payload = message.split('.')
|
||||||
header: Dict[str, Any] = jsonw.loads(base64url_decode(enc_header))
|
header: Dict[str, Any] = jsonw.loads(base64url_decode(enc_header))
|
||||||
|
@ -626,7 +677,7 @@ class Authorization:
|
||||||
raise self.server.error("JWT Expired", 401)
|
raise self.server.error("JWT Expired", 401)
|
||||||
|
|
||||||
# get user
|
# get user
|
||||||
user_info: Optional[Dict[str, Any]] = self.users.get(
|
user_info: Optional[UserInfo] = self.users.get(
|
||||||
payload.get('username', ""), None)
|
payload.get('username', ""), None)
|
||||||
if user_info is None:
|
if user_info is None:
|
||||||
raise self.server.error("Unknown user", 401)
|
raise self.server.error("Unknown user", 401)
|
||||||
|
@ -641,13 +692,13 @@ class Authorization:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Failed to decode JWT: {e}", 401
|
f"Failed to decode JWT: {e}", 401
|
||||||
) from e
|
) from e
|
||||||
return user_info
|
return user_info.as_dict()
|
||||||
|
|
||||||
def validate_api_key(self, api_key: str) -> Dict[str, Any]:
|
def validate_api_key(self, api_key: str) -> Dict[str, Any]:
|
||||||
if not self.enable_api_key:
|
if not self.enable_api_key:
|
||||||
raise self.server.error("API Key authentication is disabled", 401)
|
raise self.server.error("API Key authentication is disabled", 401)
|
||||||
if api_key and api_key == self.api_key:
|
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)
|
raise self.server.error("Invalid API Key", 401)
|
||||||
|
|
||||||
def _load_private_key(self, secret: str) -> Signer:
|
def _load_private_key(self, secret: str) -> Signer:
|
||||||
|
@ -709,7 +760,7 @@ class Authorization:
|
||||||
|
|
||||||
def _check_json_web_token(
|
def _check_json_web_token(
|
||||||
self, request: HTTPServerRequest, required: bool = True
|
self, request: HTTPServerRequest, required: bool = True
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[UserInfo]:
|
||||||
auth_token: Optional[str] = request.headers.get("Authorization")
|
auth_token: Optional[str] = request.headers.get("Authorization")
|
||||||
if auth_token is None:
|
if auth_token is None:
|
||||||
auth_token = request.headers.get("X-Access-Token")
|
auth_token = request.headers.get("X-Access-Token")
|
||||||
|
@ -757,23 +808,21 @@ class Authorization:
|
||||||
|
|
||||||
async def _check_trusted_connection(
|
async def _check_trusted_connection(
|
||||||
self, ip: Optional[IPAddr]
|
self, ip: Optional[IPAddr]
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[UserInfo]:
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
curtime = time.time()
|
curtime = time.time()
|
||||||
exp_time = curtime + TRUSTED_CONNECTION_TIMEOUT
|
exp_time = curtime + TRUSTED_CONNECTION_TIMEOUT
|
||||||
if ip in self.trusted_users:
|
if ip in self.trusted_users:
|
||||||
self.trusted_users[ip]['expires_at'] = exp_time
|
self.trusted_users[ip]["expires_at"] = exp_time
|
||||||
return self.trusted_users[ip]
|
return self.trusted_users[ip]["user"]
|
||||||
elif await self._check_authorized_ip(ip):
|
elif await self._check_authorized_ip(ip):
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Trusted Connection Detected, IP: {ip}")
|
f"Trusted Connection Detected, IP: {ip}")
|
||||||
self.trusted_users[ip] = {
|
self.trusted_users[ip] = {
|
||||||
'username': TRUSTED_USER,
|
"user": UserInfo(TRUSTED_USER, "", curtime),
|
||||||
'password': None,
|
"expires_at": exp_time
|
||||||
'created_on': curtime,
|
|
||||||
'expires_at': exp_time
|
|
||||||
}
|
}
|
||||||
return self.trusted_users[ip]
|
return self.trusted_users[ip]["user"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _check_oneshot_token(self,
|
def _check_oneshot_token(self,
|
||||||
|
@ -805,7 +854,7 @@ class Authorization:
|
||||||
# Check JSON Web Token
|
# Check JSON Web Token
|
||||||
jwt_user = self._check_json_web_token(request, auth_required)
|
jwt_user = self._check_json_web_token(request, auth_required)
|
||||||
if jwt_user is not None:
|
if jwt_user is not None:
|
||||||
return jwt_user
|
return jwt_user.as_dict()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ip = ipaddress.ip_address(request.remote_ip) # type: ignore
|
ip = ipaddress.ip_address(request.remote_ip) # type: ignore
|
||||||
|
@ -825,7 +874,7 @@ class Authorization:
|
||||||
if self.enable_api_key:
|
if self.enable_api_key:
|
||||||
key: Optional[str] = request.headers.get("X-Api-Key")
|
key: Optional[str] = request.headers.get("X-Api-Key")
|
||||||
if key and key == self.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
|
# If the force_logins option is enabled and at least one user is created
|
||||||
# then trusted user authentication is disabled
|
# then trusted user authentication is disabled
|
||||||
|
@ -837,8 +886,10 @@ class Authorization:
|
||||||
# Check if IP is trusted. If this endpoint doesn't require authentication
|
# Check if IP is trusted. If this endpoint doesn't require authentication
|
||||||
# then it is acceptable to return None
|
# then it is acceptable to return None
|
||||||
trusted_user = await self._check_trusted_connection(ip)
|
trusted_user = await self._check_trusted_connection(ip)
|
||||||
if trusted_user is not None or not auth_required:
|
if trusted_user is not None:
|
||||||
return trusted_user
|
return trusted_user.as_dict()
|
||||||
|
if not auth_required:
|
||||||
|
return None
|
||||||
|
|
||||||
raise HTTPError(401, "Unauthorized")
|
raise HTTPError(401, "Unauthorized")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue