authorization: add ldap support

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2022-06-10 07:56:27 -04:00
parent f52df8c7ed
commit 7780a8a09e
No known key found for this signature in database
GPG Key ID: 7027245FBBDDF59A
1 changed files with 63 additions and 12 deletions

View File

@ -32,13 +32,14 @@ from typing import (
Dict, Dict,
List, List,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from confighelper import ConfigHelper from confighelper import ConfigHelper
from websockets import WebRequest from websockets import WebRequest
from tornado.httputil import HTTPServerRequest from tornado.httputil import HTTPServerRequest
from tornado.web import RequestHandler from tornado.web import RequestHandler
from . import database from .database import MoonrakerDatabase as DBComp
DBComp = database.MoonrakerDatabase 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]
OneshotToken = Tuple[IPAddr, Optional[Dict[str, Any]], asyncio.Handle] OneshotToken = Tuple[IPAddr, Optional[Dict[str, Any]], asyncio.Handle]
@ -58,6 +59,7 @@ ONESHOT_TIMEOUT = 5
TRUSTED_CONNECTION_TIMEOUT = 3600 TRUSTED_CONNECTION_TIMEOUT = 3600
PRUNE_CHECK_TIME = 300. PRUNE_CHECK_TIME = 300.
AUTH_SOURCES = ["moonraker", "ldap"]
HASH_ITER = 100000 HASH_ITER = 100000
API_USER = "_API_KEY_USER_" API_USER = "_API_KEY_USER_"
TRUSTED_USER = "_TRUSTED_USER_" TRUSTED_USER = "_TRUSTED_USER_"
@ -73,6 +75,20 @@ class Authorization:
self.server = config.get_server() self.server = config.get_server()
self.login_timeout = config.getint('login_timeout', 90) self.login_timeout = config.getint('login_timeout', 90)
self.force_logins = config.getboolean('force_logins', False) self.force_logins = config.getboolean('force_logins', False)
self.default_source = config.get('default_source', "moonraker").lower()
if self.default_source not in AUTH_SOURCES:
raise config.error(
"[authorization]: option 'default_source' - Invalid "
f"value '{self.default_source}'"
)
self.ldap: Optional[MoonrakerLDAP] = None
if config.has_section("ldap"):
self.ldap = self.server.load_component(config, "ldap", None)
if self.default_source == "ldap" and self.ldap is None:
self.server.add_warning(
"[authorization]: Option 'default_source' set to 'ldap',"
" 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) database.register_local_namespace('authorized_users', forbidden=True)
self.user_db = database.wrap_namespace('authorized_users') self.user_db = database.wrap_namespace('authorized_users')
@ -252,7 +268,7 @@ class Authorization:
return self.get_oneshot_token(ip, user_info) return self.get_oneshot_token(ip, user_info)
async def _handle_login(self, web_request: WebRequest) -> Dict[str, Any]: async def _handle_login(self, web_request: WebRequest) -> Dict[str, Any]:
return self._login_jwt_user(web_request) return await self._login_jwt_user(web_request)
async def _handle_logout(self, web_request: WebRequest) -> Dict[str, str]: async def _handle_logout(self, web_request: WebRequest) -> Dict[str, str]:
user_info = web_request.get_current_user() user_info = web_request.get_current_user()
@ -288,6 +304,7 @@ class Authorization:
return { return {
'username': username, 'username': username,
'token': token, 'token': token,
'source': user_info.get("source", "moonraker"),
'action': 'user_jwt_refresh' 'action': 'user_jwt_refresh'
} }
@ -300,16 +317,18 @@ class Authorization:
if user is None: if user is None:
return { return {
'username': None, 'username': None,
'source': None,
'created_on': None, 'created_on': None,
} }
else: else:
return { return {
'username': user['username'], 'username': user['username'],
'source': user.get("source", "moonraker"),
'created_on': user.get('created_on') 'created_on': user.get('created_on')
} }
elif action == "POST": elif action == "POST":
# Create User # Create User
return self._login_jwt_user(web_request, create=True) return await self._login_jwt_user(web_request, create=True)
elif action == "DELETE": elif action == "DELETE":
# Delete User # Delete User
return self._delete_jwt_user(web_request) return self._delete_jwt_user(web_request)
@ -324,6 +343,7 @@ class Authorization:
continue continue
user_list.append({ user_list.append({
'username': user['username'], 'username': user['username'],
'source': user.get("source", "moonraker"),
'created_on': user['created_on'] 'created_on': user['created_on']
}) })
return { return {
@ -339,6 +359,9 @@ class Authorization:
if user_info is None: if user_info is None:
raise self.server.error("No Current User") raise self.server.error("No Current User")
username = user_info['username'] username = user_info['username']
if user_info.get("source", "moonraker") == "ldap":
raise self.server.error(
f"Can´t Reset password for ldap user {username}")
if username in RESERVED_USERS: if username in RESERVED_USERS:
raise self.server.error( raise self.server.error(
f"Invalid Reset Request for user {username}") f"Invalid Reset Request for user {username}")
@ -356,16 +379,30 @@ class Authorization:
'action': "user_password_reset" 'action': "user_password_reset"
} }
def _login_jwt_user(self, async def _login_jwt_user(
web_request: WebRequest, self, web_request: WebRequest, create: bool = False
create: bool = False ) -> Dict[str, Any]:
) -> Dict[str, Any]:
username: str = web_request.get_str('username') username: str = web_request.get_str('username')
password: str = web_request.get_str('password') password: str = web_request.get_str('password')
source: str = web_request.get_str(
'source', self.default_source
).lower()
if source not in AUTH_SOURCES:
raise self.server.error(f"Invalid 'source': {source}")
user_info: Dict[str, Any] user_info: Dict[str, Any]
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}")
if source == "ldap":
if create:
raise self.server.error("Cannot Create LDAP User")
if self.ldap is None:
raise self.server.error(
"LDAP authentication not available", 401
)
await self.ldap.authenticate_ldap_user(username, password)
if username not in self.users:
create = True
if create: if create:
if username in self.users: if username in self.users:
raise self.server.error(f"User {username} already exists") raise self.server.error(f"User {username} already exists")
@ -376,21 +413,32 @@ class Authorization:
'username': username, 'username': username,
'password': hashed_pass, 'password': hashed_pass,
'salt': salt.hex(), 'salt': salt.hex(),
'source': source,
'created_on': time.time() 'created_on': time.time()
} }
self.users[username] = user_info self.users[username] = user_info
self._sync_user(username) self._sync_user(username)
action = "user_created" action = "user_created"
if source == "ldap":
# Dont notify user created
action = "user_logged_in"
create = False
else: else:
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")
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( 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.get('jwt_secret', None)
if jwt_secret_hex is None: if jwt_secret_hex is None:
private_key = Signer() private_key = Signer()
@ -416,6 +464,7 @@ class Authorization:
return { return {
'username': username, 'username': username,
'token': token, 'token': token,
'source': user_info.get("source", "moonraker"),
'refresh_token': refresh_token, 'refresh_token': refresh_token,
'action': action 'action': action
} }
@ -641,8 +690,10 @@ class Authorization:
def check_authorized(self, def check_authorized(self,
request: HTTPServerRequest request: HTTPServerRequest
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
if request.path in self.permitted_paths or \ if (
request.method == "OPTIONS": request.path in self.permitted_paths
or request.method == "OPTIONS"
):
return None return None
# Check JSON Web Token # Check JSON Web Token