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,
|
||||
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
|
||||
) -> Dict[str, Any]:
|
||||
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,21 +413,32 @@ 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()
|
||||
action = "user_logged_in"
|
||||
if hashed_pass != user_info['password']:
|
||||
raise self.server.error("Invalid Password")
|
||||
if hashed_pass != user_info['password']:
|
||||
raise self.server.error("Invalid Password")
|
||||
jwt_secret_hex: Optional[str] = user_info.get('jwt_secret', None)
|
||||
if jwt_secret_hex is None:
|
||||
private_key = Signer()
|
||||
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue