authorization: remove "permitted_paths" attribute
Track authentication requirements in the API Definition. This eliminates the need to look up the authentication component to disable auth on an endpoint. Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
eed759e111
commit
b3b60757aa
|
@ -311,6 +311,7 @@ class MoonrakerApp:
|
|||
transports: Union[List[str], TransportType] = TransportType.all(),
|
||||
wrap_result: bool = True,
|
||||
content_type: Optional[str] = None,
|
||||
auth_required: bool = True,
|
||||
is_remote: bool = False
|
||||
) -> None:
|
||||
if isinstance(request_types, list):
|
||||
|
@ -318,7 +319,7 @@ class MoonrakerApp:
|
|||
if isinstance(transports, list):
|
||||
transports = TransportType.from_string_list(transports)
|
||||
api_def = APIDefinition.create(
|
||||
endpoint, request_types, callback, transports, is_remote
|
||||
endpoint, request_types, callback, transports, auth_required, is_remote
|
||||
)
|
||||
http_path = api_def.http_path
|
||||
if http_path in self.registered_base_handlers:
|
||||
|
@ -414,7 +415,7 @@ class MoonrakerApp:
|
|||
class AuthorizedRequestHandler(tornado.web.RequestHandler):
|
||||
def initialize(self) -> None:
|
||||
self.server: Server = self.settings['server']
|
||||
self.endpoint: str = ""
|
||||
self.auth_required: bool = True
|
||||
|
||||
def set_default_headers(self) -> None:
|
||||
origin: Optional[str] = self.request.headers.get("Origin")
|
||||
|
@ -427,12 +428,11 @@ class AuthorizedRequestHandler(tornado.web.RequestHandler):
|
|||
self.cors_enabled = auth.check_cors(origin, self)
|
||||
|
||||
def prepare(self) -> None:
|
||||
app: MoonrakerApp = self.server.lookup_component("application")
|
||||
if not self.endpoint:
|
||||
self.endpoint = app.parse_endpoint(self.request.path or "")
|
||||
auth: AuthComp = self.server.lookup_component('authorization', None)
|
||||
if auth is not None:
|
||||
self.current_user = auth.check_authorized(self.request, self.endpoint)
|
||||
self.current_user = auth.authenticate_request(
|
||||
self.request, self.auth_required
|
||||
)
|
||||
|
||||
def options(self, *args, **kwargs) -> None:
|
||||
# Enable CORS if configured
|
||||
|
@ -476,7 +476,6 @@ class AuthorizedFileHandler(tornado.web.StaticFileHandler):
|
|||
) -> None:
|
||||
super(AuthorizedFileHandler, self).initialize(path, default_filename)
|
||||
self.server: Server = self.settings['server']
|
||||
self.endpoint: str = ""
|
||||
|
||||
def set_default_headers(self) -> None:
|
||||
origin: Optional[str] = self.request.headers.get("Origin")
|
||||
|
@ -489,11 +488,11 @@ class AuthorizedFileHandler(tornado.web.StaticFileHandler):
|
|||
self.cors_enabled = auth.check_cors(origin, self)
|
||||
|
||||
def prepare(self) -> None:
|
||||
app: MoonrakerApp = self.server.lookup_component("application")
|
||||
self.endpoint = app.parse_endpoint(self.request.path or "")
|
||||
auth: AuthComp = self.server.lookup_component('authorization', None)
|
||||
if auth is not None and self._check_need_auth():
|
||||
self.current_user = auth.check_authorized(self.request, self.endpoint)
|
||||
if auth is not None:
|
||||
self.current_user = auth.authenticate_request(
|
||||
self.request, self._check_need_auth()
|
||||
)
|
||||
|
||||
def options(self, *args, **kwargs) -> None:
|
||||
# Enable CORS if configured
|
||||
|
@ -531,7 +530,7 @@ class DynamicRequestHandler(AuthorizedRequestHandler):
|
|||
self.api_defintion = api_definition
|
||||
self.wrap_result = wrap_result
|
||||
self.content_type = content_type
|
||||
self.endpoint = api_definition.endpoint
|
||||
self.auth_required = api_definition.auth_required
|
||||
|
||||
# Converts query string values with type hints
|
||||
def _convert_type(self, value: str, hint: str) -> Any:
|
||||
|
@ -601,10 +600,11 @@ class DynamicRequestHandler(AuthorizedRequestHandler):
|
|||
def _log_debug(self, header: str, args: Any) -> None:
|
||||
if self.server.is_verbose_enabled():
|
||||
resp = args
|
||||
endpoint = self.api_defintion.endpoint
|
||||
if isinstance(args, dict):
|
||||
if (
|
||||
self.endpoint.startswith("/access") or
|
||||
self.endpoint.startswith("/machine/sudo/password")
|
||||
endpoint.startswith("/access") or
|
||||
endpoint.startswith("/machine/sudo/password")
|
||||
):
|
||||
resp = {key: "<sanitized>" for key in args}
|
||||
elif isinstance(args, str):
|
||||
|
@ -663,7 +663,9 @@ class FileRequestHandler(AuthorizedFileHandler):
|
|||
f"filename*=UTF-8\'\'{utf8_basename}")
|
||||
|
||||
async def delete(self, path: str) -> None:
|
||||
path = self.endpoint.lstrip("/").split("/", 2)[-1]
|
||||
app: MoonrakerApp = self.server.lookup_component("application")
|
||||
endpoint = app.parse_endpoint(self.request.path or "")
|
||||
path = endpoint.lstrip("/").split("/", 2)[-1]
|
||||
path = url_unescape(path, plus=False)
|
||||
file_manager: FileManager
|
||||
file_manager = self.server.lookup_component('file_manager')
|
||||
|
@ -944,6 +946,10 @@ class AuthorizedErrorHandler(AuthorizedRequestHandler):
|
|||
self.finish(jsonw.dumps({'error': err}))
|
||||
|
||||
class RedirectHandler(AuthorizedRequestHandler):
|
||||
def initialize(self) -> None:
|
||||
super().initialize()
|
||||
self.auth_required = False
|
||||
|
||||
def get(self, *args, **kwargs) -> None:
|
||||
url: Optional[str] = self.get_argument('url', None)
|
||||
if url is None:
|
||||
|
@ -972,7 +978,7 @@ class WelcomeHandler(tornado.web.RequestHandler):
|
|||
auth: AuthComp = self.server.lookup_component("authorization", None)
|
||||
if auth is not None:
|
||||
try:
|
||||
auth.check_authorized(self.request)
|
||||
auth.authenticate_request(self.request)
|
||||
except tornado.web.HTTPError:
|
||||
authorized = False
|
||||
else:
|
||||
|
|
|
@ -168,6 +168,7 @@ class APIDefinition:
|
|||
request_types: RequestType
|
||||
transports: TransportType
|
||||
callback: Callable[[WebRequest], Coroutine]
|
||||
auth_required: bool
|
||||
_cache: ClassVar[Dict[str, APIDefinition]] = {}
|
||||
|
||||
def request(
|
||||
|
@ -196,6 +197,7 @@ class APIDefinition:
|
|||
request_types: Union[List[str], RequestType],
|
||||
callback: Callable[[WebRequest], Coroutine],
|
||||
transports: Union[List[str], TransportType] = TransportType.all(),
|
||||
auth_required: bool = True,
|
||||
is_remote: bool = False
|
||||
) -> APIDefinition:
|
||||
if isinstance(request_types, list):
|
||||
|
@ -239,7 +241,7 @@ class APIDefinition:
|
|||
|
||||
api_def = cls(
|
||||
endpoint, http_path, rpc_methods, request_types,
|
||||
transports, callback
|
||||
transports, callback, auth_required
|
||||
)
|
||||
cls._cache[endpoint] = api_def
|
||||
return api_def
|
||||
|
@ -335,7 +337,7 @@ class BaseRemoteConnection(APITransport):
|
|||
def screen_rpc_request(
|
||||
self, api_def: APIDefinition, req_type: RequestType, args: Dict[str, Any]
|
||||
) -> None:
|
||||
self.check_authenticated(api_def.endpoint)
|
||||
self.check_authenticated(api_def)
|
||||
|
||||
async def _process_message(self, message: str) -> None:
|
||||
try:
|
||||
|
@ -366,16 +368,16 @@ class BaseRemoteConnection(APITransport):
|
|||
self.user_info = auth.validate_jwt(token)
|
||||
elif api_key is not None and self.user_info is None:
|
||||
self.user_info = auth.validate_api_key(api_key)
|
||||
else:
|
||||
self.check_authenticated()
|
||||
elif self._need_auth:
|
||||
raise self.server.error("Unauthorized", 401)
|
||||
|
||||
def check_authenticated(self, path: str = "") -> None:
|
||||
def check_authenticated(self, api_def: APIDefinition) -> None:
|
||||
if not self._need_auth:
|
||||
return
|
||||
auth: AuthComp = self.server.lookup_component("authorization", None)
|
||||
if auth is None:
|
||||
return
|
||||
if not auth.is_path_permitted(path):
|
||||
if api_def.auth_required:
|
||||
raise self.server.error("Unauthorized", 401)
|
||||
|
||||
def on_user_logout(self, user: str) -> bool:
|
||||
|
|
|
@ -27,7 +27,6 @@ from typing import (
|
|||
TYPE_CHECKING,
|
||||
Any,
|
||||
Tuple,
|
||||
Set,
|
||||
Optional,
|
||||
Union,
|
||||
Dict,
|
||||
|
@ -152,7 +151,6 @@ class Authorization:
|
|||
self.user_db.sync(self.users)
|
||||
self.trusted_users: Dict[IPAddr, Any] = {}
|
||||
self.oneshot_tokens: Dict[str, OneshotToken] = {}
|
||||
self.permitted_paths: Set[str] = set()
|
||||
|
||||
# Get allowed cors domains
|
||||
self.cors_domains: List[str] = []
|
||||
|
@ -222,13 +220,10 @@ class Authorization:
|
|||
self._prune_conn_handler)
|
||||
|
||||
# Register Authorization Endpoints
|
||||
self.permitted_paths.add("/server/redirect")
|
||||
self.permitted_paths.add("/access/login")
|
||||
self.permitted_paths.add("/access/refresh_jwt")
|
||||
self.permitted_paths.add("/access/info")
|
||||
self.server.register_endpoint(
|
||||
"/access/login", RequestType.POST, self._handle_login,
|
||||
transports=TransportType.HTTP | TransportType.WEBSOCKET
|
||||
transports=TransportType.HTTP | TransportType.WEBSOCKET,
|
||||
auth_required=False
|
||||
)
|
||||
self.server.register_endpoint(
|
||||
"/access/logout", RequestType.POST, self._handle_logout,
|
||||
|
@ -236,7 +231,8 @@ class Authorization:
|
|||
)
|
||||
self.server.register_endpoint(
|
||||
"/access/refresh_jwt", RequestType.POST, self._handle_refresh_jwt,
|
||||
transports=TransportType.HTTP | TransportType.WEBSOCKET
|
||||
transports=TransportType.HTTP | TransportType.WEBSOCKET,
|
||||
auth_required=False
|
||||
)
|
||||
self.server.register_endpoint(
|
||||
"/access/user", RequestType.all(), self._handle_user_request,
|
||||
|
@ -261,7 +257,8 @@ class Authorization:
|
|||
)
|
||||
self.server.register_endpoint(
|
||||
"/access/info", RequestType.GET, self._handle_info_request,
|
||||
transports=TransportType.HTTP | TransportType.WEBSOCKET
|
||||
transports=TransportType.HTTP | TransportType.WEBSOCKET,
|
||||
auth_required=False
|
||||
)
|
||||
wsm: WebsocketManager = self.server.lookup_component("websockets")
|
||||
wsm.register_notification("authorization:user_created")
|
||||
|
@ -272,12 +269,6 @@ class Authorization:
|
|||
"authorization:user_logged_out", event_type="logout"
|
||||
)
|
||||
|
||||
def register_permited_path(self, path: str) -> None:
|
||||
self.permitted_paths.add(path)
|
||||
|
||||
def is_path_permitted(self, path: str) -> bool:
|
||||
return path in self.permitted_paths
|
||||
|
||||
def _sync_user(self, username: str) -> None:
|
||||
self.user_db[username] = self.users[username]
|
||||
|
||||
|
@ -770,13 +761,10 @@ class Authorization:
|
|||
return False
|
||||
return self.failed_logins.get(ip_addr, 0) >= self.max_logins
|
||||
|
||||
def check_authorized(
|
||||
self, request: HTTPServerRequest, endpoint: str = "",
|
||||
def authenticate_request(
|
||||
self, request: HTTPServerRequest, auth_required: bool = True
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if (
|
||||
endpoint in self.permitted_paths
|
||||
or request.method == "OPTIONS"
|
||||
):
|
||||
if request.method == "OPTIONS":
|
||||
return None
|
||||
|
||||
# Check JSON Web Token
|
||||
|
@ -804,14 +792,17 @@ class Authorization:
|
|||
if key and key == self.api_key:
|
||||
return self.users[API_USER]
|
||||
|
||||
# If the force_logins option is enabled and at least one
|
||||
# user is created this is an unauthorized request
|
||||
# If the force_logins option is enabled and at least one user is created
|
||||
# then trusted user authentication is disabled
|
||||
if self.force_logins and len(self.users) > 1:
|
||||
if not auth_required:
|
||||
return None
|
||||
raise HTTPError(401, "Unauthorized, Force Logins Enabled")
|
||||
|
||||
# Check if IP is trusted
|
||||
# Check if IP is trusted. If this endpoint doesn't require authentication
|
||||
# then it is acceptable to return None
|
||||
trusted_user = self._check_trusted_connection(ip)
|
||||
if trusted_user is not None:
|
||||
if trusted_user is not None or not auth_required:
|
||||
return trusted_user
|
||||
|
||||
raise HTTPError(401, "Unauthorized")
|
||||
|
|
|
@ -47,7 +47,6 @@ if TYPE_CHECKING:
|
|||
from .shell_command import ShellCommandFactory as SCMDComp
|
||||
from .database import MoonrakerDatabase
|
||||
from .file_manager.file_manager import FileManager
|
||||
from .authorization import Authorization
|
||||
from .announcements import Announcements
|
||||
from .proc_stats import ProcStats
|
||||
from .dbus_manager import DbusManager
|
||||
|
@ -1933,11 +1932,6 @@ class InstallValidator:
|
|||
if self._sudo_requested:
|
||||
return
|
||||
self._sudo_requested = True
|
||||
auth: Optional[Authorization]
|
||||
auth = self.server.lookup_component("authorization", None)
|
||||
if auth is not None:
|
||||
# Bypass authentication requirements
|
||||
auth.register_permited_path("/machine/sudo/password")
|
||||
machine: Machine = self.server.lookup_component("machine")
|
||||
machine.register_sudo_request(
|
||||
self._on_password_received,
|
||||
|
|
|
@ -30,7 +30,6 @@ if TYPE_CHECKING:
|
|||
from ..confighelper import ConfigHelper
|
||||
from ..common import WebRequest
|
||||
from ..app import MoonrakerApp
|
||||
from .authorization import Authorization
|
||||
from .machine import Machine
|
||||
|
||||
ZC_SERVICE_TYPE = "_moonraker._tcp.local."
|
||||
|
@ -209,17 +208,14 @@ class SSDPServer(asyncio.protocols.DatagramProtocol):
|
|||
self.boot_id = int(eventloop.get_loop_time())
|
||||
self.config_id = 1
|
||||
self.ad_timer = eventloop.register_timer(self._advertise_presence)
|
||||
auth: Optional[Authorization]
|
||||
auth = self.server.load_component(config, "authorization", None)
|
||||
if auth is not None:
|
||||
auth.register_permited_path("/server/zeroconf/ssdp")
|
||||
self.server.register_endpoint(
|
||||
"/server/zeroconf/ssdp",
|
||||
RequestType.GET,
|
||||
self._handle_xml_request,
|
||||
transports=TransportType.HTTP,
|
||||
wrap_result=False,
|
||||
content_type="application/xml"
|
||||
content_type="application/xml",
|
||||
auth_required=False
|
||||
)
|
||||
|
||||
def _create_ssdp_socket(
|
||||
|
|
|
@ -57,7 +57,7 @@ class WebsocketManager:
|
|||
)
|
||||
self.server.register_endpoint(
|
||||
"/server/connection/identify", RequestType.POST, self._handle_identify,
|
||||
TransportType.WEBSOCKET
|
||||
TransportType.WEBSOCKET, auth_required=False
|
||||
)
|
||||
self.server.register_component("websockets", self)
|
||||
|
||||
|
@ -321,7 +321,7 @@ class WebSocket(WebSocketHandler, BaseRemoteConnection):
|
|||
auth: AuthComp = self.server.lookup_component('authorization', None)
|
||||
if auth is not None:
|
||||
try:
|
||||
self._user_info = auth.check_authorized(self.request)
|
||||
self._user_info = auth.authenticate_request(self.request)
|
||||
except Exception as e:
|
||||
logging.info(f"Websocket Failed Authentication: {e}")
|
||||
self._user_info = None
|
||||
|
@ -461,7 +461,7 @@ class BridgeSocket(WebSocketHandler):
|
|||
)
|
||||
auth: AuthComp = self.server.lookup_component("authorization", None)
|
||||
if auth is not None:
|
||||
self.current_user = auth.check_authorized(self.request)
|
||||
self.current_user = auth.authenticate_request(self.request)
|
||||
kconn: Klippy = self.server.lookup_component("klippy_connection")
|
||||
try:
|
||||
reader, writer = await kconn.open_klippy_connection()
|
||||
|
|
Loading…
Reference in New Issue