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,
List,
)
if TYPE_CHECKING:
from confighelper import ConfigHelper
from websockets import WebRequest
from tornado.httputil import HTTPServerRequest
from tornado.web import RequestHandler
from . import database
DBComp = database.MoonrakerDatabase
from .database import MoonrakerDatabase as DBComp
from .ldap import MoonrakerLDAP
IPAddr = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
IPNetwork = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
OneshotToken = Tuple[IPAddr, Optional[Dict[str, Any]], asyncio.Handle]
@ -58,6 +59,7 @@ ONESHOT_TIMEOUT = 5
TRUSTED_CONNECTION_TIMEOUT = 3600
PRUNE_CHECK_TIME = 300.
AUTH_SOURCES = ["moonraker", "ldap"]
HASH_ITER = 100000
API_USER = "_API_KEY_USER_"
TRUSTED_USER = "_TRUSTED_USER_"
@ -73,6 +75,20 @@ class Authorization:
self.server = config.get_server()
self.login_timeout = config.getint('login_timeout', 90)
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.register_local_namespace('authorized_users', forbidden=True)
self.user_db = database.wrap_namespace('authorized_users')
@ -252,7 +268,7 @@ class Authorization:
return self.get_oneshot_token(ip, user_info)
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]:
user_info = web_request.get_current_user()
@ -288,6 +304,7 @@ class Authorization:
return {
'username': username,
'token': token,
'source': user_info.get("source", "moonraker"),
'action': 'user_jwt_refresh'
}
@ -300,16 +317,18 @@ class Authorization:
if user is None:
return {
'username': None,
'source': None,
'created_on': None,
}
else:
return {
'username': user['username'],
'source': user.get("source", "moonraker"),
'created_on': user.get('created_on')
}
elif action == "POST":
# Create User
return self._login_jwt_user(web_request, create=True)
return await self._login_jwt_user(web_request, create=True)
elif action == "DELETE":
# Delete User
return self._delete_jwt_user(web_request)
@ -324,6 +343,7 @@ class Authorization:
continue
user_list.append({
'username': user['username'],
'source': user.get("source", "moonraker"),
'created_on': user['created_on']
})
return {
@ -339,6 +359,9 @@ class Authorization:
if user_info is None:
raise self.server.error("No Current User")
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:
raise self.server.error(
f"Invalid Reset Request for user {username}")
@ -356,16 +379,30 @@ class Authorization:
'action': "user_password_reset"
}
def _login_jwt_user(self,
web_request: WebRequest,
create: bool = False
async def _login_jwt_user(
self, web_request: WebRequest, create: bool = False
) -> Dict[str, Any]:
username: str = web_request.get_str('username')
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]
if username in RESERVED_USERS:
raise self.server.error(
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 username in self.users:
raise self.server.error(f"User {username} already exists")
@ -376,15 +413,26 @@ class Authorization:
'username': username,
'password': hashed_pass,
'salt': salt.hex(),
'source': source,
'created_on': time.time()
}
self.users[username] = user_info
self._sync_user(username)
action = "user_created"
if source == "ldap":
# Dont notify user created
action = "user_logged_in"
create = False
else:
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")
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'])
hashed_pass = hashlib.pbkdf2_hmac(
'sha256', password.encode(), salt, HASH_ITER).hex()
@ -416,6 +464,7 @@ class Authorization:
return {
'username': username,
'token': token,
'source': user_info.get("source", "moonraker"),
'refresh_token': refresh_token,
'action': action
}
@ -641,8 +690,10 @@ class Authorization:
def check_authorized(self,
request: HTTPServerRequest
) -> Optional[Dict[str, Any]]:
if request.path in self.permitted_paths or \
request.method == "OPTIONS":
if (
request.path in self.permitted_paths
or request.method == "OPTIONS"
):
return None
# Check JSON Web Token