authorization: convert module to component

CONFIG CHANGE:  This deprecates the "enabled" option in the [authorization] section.  Authorization will be enabled if the section is included in moonraker.conf,  otherwise it will be disabled.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2021-04-09 08:45:40 -04:00
parent 57836047f6
commit 14991ac3b9
4 changed files with 86 additions and 89 deletions

View File

@ -18,8 +18,6 @@ from tornado.routing import Rule, PathMatches, AnyMatches
from tornado.log import access_log from tornado.log import access_log
from utils import ServerError from utils import ServerError
from websockets import WebRequest, WebsocketManager, WebSocket from websockets import WebRequest, WebsocketManager, WebSocket
from authorization import AuthorizedRequestHandler, AuthorizedFileHandler
from authorization import Authorization
from streaming_form_data import StreamingFormDataParser from streaming_form_data import StreamingFormDataParser
from streaming_form_data.targets import FileTarget, ValueTarget from streaming_form_data.targets import FileTarget, ValueTarget
@ -89,7 +87,6 @@ class MoonrakerApp:
# Set Up Websocket and Authorization Managers # Set Up Websocket and Authorization Managers
self.wsm = WebsocketManager(self.server) self.wsm = WebsocketManager(self.server)
self.auth = Authorization(config['authorization'])
mimetypes.add_type('text/plain', '.log') mimetypes.add_type('text/plain', '.log')
mimetypes.add_type('text/plain', '.gcode') mimetypes.add_type('text/plain', '.gcode')
@ -123,7 +120,6 @@ class MoonrakerApp:
"moonraker.log", logfile, force=True) "moonraker.log", logfile, force=True)
self.register_static_file_handler( self.register_static_file_handler(
"klippy.log", DEFAULT_KLIPPY_LOG_PATH, force=True) "klippy.log", DEFAULT_KLIPPY_LOG_PATH, force=True)
self.auth.register_handlers(self)
def listen(self, host, port): def listen(self, host, port):
self.tornado_server = self.app.listen( self.tornado_server = self.app.listen(
@ -148,9 +144,6 @@ class MoonrakerApp:
def get_server(self): def get_server(self):
return self.server return self.server
def get_auth(self):
return self.auth
def get_websocket_manager(self): def get_websocket_manager(self):
return self.wsm return self.wsm
@ -159,7 +152,6 @@ class MoonrakerApp:
self.tornado_server.stop() self.tornado_server.stop()
await self.tornado_server.close_all_connections() await self.tornado_server.close_all_connections()
await self.wsm.close() await self.wsm.close()
self.auth.close()
def register_remote_handler(self, endpoint): def register_remote_handler(self, endpoint):
if endpoint in RESERVED_ENDPOINTS: if endpoint in RESERVED_ENDPOINTS:
@ -265,6 +257,78 @@ class MoonrakerApp:
self.api_cache[endpoint] = api_def self.api_cache[endpoint] = api_def
return api_def return api_def
class AuthorizedRequestHandler(tornado.web.RequestHandler):
def initialize(self):
self.server = self.settings['parent'].get_server()
def set_default_headers(self):
origin = self.request.headers.get("Origin")
# it is necessary to look up the parent app here,
# as initialize() may not yet be called
server = self.settings['parent'].get_server()
auth = server.lookup_component('authorization', None)
self.cors_enabled = False
if auth is not None:
self.cors_enabled = auth.check_cors(origin, self)
def prepare(self):
auth = self.server.lookup_component('authorization', None)
if auth is not None and not auth.check_authorized(self.request):
raise tornado.web.HTTPError(401, "Unauthorized")
def options(self, *args, **kwargs):
# Enable CORS if configured
if self.cors_enabled:
self.set_status(204)
self.finish()
else:
super(AuthorizedRequestHandler, self).options()
def get_associated_websocket(self):
# Return associated websocket connection if an id
# was provided by the request
conn = None
conn_id = self.get_argument('connection_id', None)
if conn_id is not None:
try:
conn_id = int(conn_id)
except Exception:
pass
else:
wsm = self.settings['parent'].get_websocket_manager()
conn = wsm.get_websocket(conn_id)
return conn
# Due to the way Python treats multiple inheritance its best
# to create a separate authorized handler for serving files
class AuthorizedFileHandler(tornado.web.StaticFileHandler):
def initialize(self, path, default_filename=None):
super(AuthorizedFileHandler, self).initialize(path, default_filename)
self.server = self.settings['parent'].get_server()
def set_default_headers(self):
origin = self.request.headers.get("Origin")
# it is necessary to look up the parent app here,
# as initialize() may not yet be called
server = self.settings['parent'].get_server()
auth = server.lookup_component('authorization', None)
self.cors_enabled = False
if auth is not None:
self.cors_enabled = auth.check_cors(origin, self)
def prepare(self):
auth = self.server.lookup_component('authorization', None)
if auth is not None and not auth.check_authorized(self.request):
raise tornado.web.HTTPError(401, "Unauthorized")
def options(self, *args, **kwargs):
# Enable CORS if configured
if self.cors_enabled:
self.set_status(204)
self.finish()
else:
super(AuthorizedFileHandler, self).options()
class DynamicRequestHandler(AuthorizedRequestHandler): class DynamicRequestHandler(AuthorizedRequestHandler):
def initialize(self, callback, methods, need_object_parser=False, def initialize(self, callback, methods, need_object_parser=False,
is_remote=True, wrap_result=True): is_remote=True, wrap_result=True):

View File

@ -10,7 +10,6 @@ import time
import ipaddress import ipaddress
import re import re
import logging import logging
import tornado
from tornado.ioloop import IOLoop, PeriodicCallback from tornado.ioloop import IOLoop, PeriodicCallback
from utils import ServerError from utils import ServerError
@ -20,10 +19,10 @@ PRUNE_CHECK_TIME = 300 * 1000
class Authorization: class Authorization:
def __init__(self, config): def __init__(self, config):
self.server = config.get_server()
api_key_file = config.get('api_key_file', "~/.moonraker_api_key") api_key_file = config.get('api_key_file', "~/.moonraker_api_key")
self.api_key_file = os.path.expanduser(api_key_file) self.api_key_file = os.path.expanduser(api_key_file)
self.api_key = self._read_api_key() self.api_key = self._read_api_key()
self.auth_enabled = config.getboolean('enabled', True)
self.trusted_connections = {} self.trusted_connections = {}
self.access_tokens = {} self.access_tokens = {}
@ -70,7 +69,6 @@ class Authorization:
logging.info( logging.info(
f"Authorization Configuration Loaded\n" f"Authorization Configuration Loaded\n"
f"Auth Enabled: {self.auth_enabled}\n"
f"Trusted Clients:\n{t_clients}\n" f"Trusted Clients:\n{t_clients}\n"
f"CORS Domains:\n{c_domains}") f"CORS Domains:\n{c_domains}")
@ -78,12 +76,11 @@ class Authorization:
self._prune_conn_handler, PRUNE_CHECK_TIME) self._prune_conn_handler, PRUNE_CHECK_TIME)
self.prune_handler.start() self.prune_handler.start()
def register_handlers(self, app):
# Register Authorization Endpoints # Register Authorization Endpoints
app.register_local_handler( self.server.register_endpoint(
"/access/api_key", ['GET', 'POST'], "/access/api_key", ['GET', 'POST'],
self._handle_apikey_request, protocol=['http']) self._handle_apikey_request, protocol=['http'])
app.register_local_handler( self.server.register_endpoint(
"/access/oneshot_token", ['GET'], "/access/oneshot_token", ['GET'],
self._handle_token_request, protocol=['http']) self._handle_token_request, protocol=['http'])
@ -136,9 +133,6 @@ class Authorization:
def _token_expire_handler(self, token): def _token_expire_handler(self, token):
self.access_tokens.pop(token, None) self.access_tokens.pop(token, None)
def is_enabled(self):
return self.auth_enabled
def get_access_token(self): def get_access_token(self):
token = base64.b32encode(os.urandom(20)).decode() token = base64.b32encode(os.urandom(20)).decode()
ioloop = IOLoop.current() ioloop = IOLoop.current()
@ -167,10 +161,6 @@ class Authorization:
return False return False
def check_authorized(self, request): def check_authorized(self, request):
# Authorization is disabled, request may pass
if not self.auth_enabled:
return True
# Check if IP is trusted # Check if IP is trusted
try: try:
ip = ipaddress.ip_address(request.remote_ip) ip = ipaddress.ip_address(request.remote_ip)
@ -240,68 +230,6 @@ class Authorization:
def close(self): def close(self):
self.prune_handler.stop() self.prune_handler.stop()
class AuthorizedRequestHandler(tornado.web.RequestHandler):
def initialize(self):
app = self.settings['parent']
self.server = app.get_server()
self.auth = app.get_auth()
self.wsm = app.get_websocket_manager()
def set_default_headers(self): def load_component(config):
origin = self.request.headers.get("Origin") return Authorization(config)
# it is necessary to look up the parent app here,
# as initialize() may not yet be called
auth = self.settings['parent'].get_auth()
self.cors_enabled = auth.check_cors(origin, self)
def prepare(self):
if not self.auth.check_authorized(self.request):
raise tornado.web.HTTPError(401, "Unauthorized")
def options(self, *args, **kwargs):
# Enable CORS if configured
if self.cors_enabled:
self.set_status(204)
self.finish()
else:
super(AuthorizedRequestHandler, self).options()
def get_associated_websocket(self):
# Return associated websocket connection if an id
# was provided by the request
conn = None
conn_id = self.get_argument('connection_id', None)
if conn_id is not None:
try:
conn_id = int(conn_id)
except Exception:
pass
else:
conn = self.wsm.get_websocket(conn_id)
return conn
# Due to the way Python treats multiple inheritance its best
# to create a separate authorized handler for serving files
class AuthorizedFileHandler(tornado.web.StaticFileHandler):
def initialize(self, path, default_filename=None):
super(AuthorizedFileHandler, self).initialize(path, default_filename)
app = self.settings['parent']
self.server = app.get_server()
self.auth = app.get_auth()
def set_default_headers(self):
origin = self.request.headers.get("Origin")
auth = self.settings['parent'].get_auth()
self.cors_enabled = auth.check_cors(origin, self)
def prepare(self):
if not self.auth.check_authorized(self.request):
raise tornado.web.HTTPError(401, "Unauthorized")
def options(self, *args, **kwargs):
# Enable CORS if configured
if self.cors_enabled:
self.set_status(204)
self.finish()
else:
super(AuthorizedFileHandler, self).options()

View File

@ -138,7 +138,7 @@ class Server:
# check for optional components # check for optional components
opt_sections = set([s.split()[0] for s in config.sections()]) - \ opt_sections = set([s.split()[0] for s in config.sections()]) - \
set(['server', 'authorization', 'system_args']) set(['server', 'system_args'])
for section in opt_sections: for section in opt_sections:
self.load_component(config, section, None) self.load_component(config, section, None)

View File

@ -259,7 +259,7 @@ class WebsocketManager:
class WebSocket(WebSocketHandler): class WebSocket(WebSocketHandler):
def initialize(self): def initialize(self):
app = self.settings['parent'] app = self.settings['parent']
self.auth = app.get_auth() self.server = app.get_server()
self.wsm = app.get_websocket_manager() self.wsm = app.get_websocket_manager()
self.rpc = self.wsm.rpc self.rpc = self.wsm.rpc
self.uid = id(self) self.uid = id(self)
@ -303,9 +303,14 @@ class WebSocket(WebSocketHandler):
def check_origin(self, origin): def check_origin(self, origin):
if not super(WebSocket, self).check_origin(origin): if not super(WebSocket, self).check_origin(origin):
return self.auth.check_cors(origin) auth = self.server.lookup_component('authorization', None)
if auth is not None:
return auth.check_cors(origin)
return False
return True return True
# Check Authorized User # Check Authorized User
def prepare(self): def prepare(self):
if not self.auth.check_authorized(self.request): auth = self.server.lookup_component('authorization', None)
if auth is not None and not auth.check_authorized(self.request):
raise tornado.web.HTTPError(401, "Unauthorized") raise tornado.web.HTTPError(401, "Unauthorized")