authorization: add ldap support
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
f52df8c7ed
commit
7780a8a09e
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue