diff --git a/htdocs/pwchange.html b/htdocs/pwchange.html new file mode 100644 index 00000000..e3c433ad --- /dev/null +++ b/htdocs/pwchange.html @@ -0,0 +1,32 @@ + + + + OpenWebRX Password change + + + + + + + + +${header} +
+
+
+ Your password has been automatically generated and must be changed in order to proceed. +
+
+
+ + +
+
+ + +
+ +
+
+
+ \ No newline at end of file diff --git a/owrx/controllers/admin.py b/owrx/controllers/admin.py index 5ce4a066..822bf630 100644 --- a/owrx/controllers/admin.py +++ b/owrx/controllers/admin.py @@ -10,33 +10,36 @@ logger = logging.getLogger(__name__) class Authentication(object): - def isAuthenticated(self, request): + def getUser(self, request): if "owrx-session" not in request.cookies: - return False + return None session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value) if session is None: - return False + return None if "user" not in session: - return False + return None userList = UserList.getSharedInstance() try: - user = userList[session["user"]] - return user.is_enabled() + return userList[session["user"]] except KeyError: - return False + return None class AdminController(WebpageController): def __init__(self, handler, request, options): self.authentication = Authentication() + self.user = self.authentication.getUser(request) super().__init__(handler, request, options) + def isAuthorized(self): + return self.user is not None and self.user.is_enabled() and not self.user.must_change_password + def handle_request(self): config = Config.get() if "webadmin_enabled" not in config or not config["webadmin_enabled"]: self.send_response("Web Admin is disabled", code=403) return - if self.authentication.isAuthenticated(self.request): + if self.isAuthorized(): super().handle_request() else: target = "/login?{0}".format(parse.urlencode({"ref": self.request.path})) diff --git a/owrx/controllers/profile.py b/owrx/controllers/profile.py new file mode 100644 index 00000000..6542553d --- /dev/null +++ b/owrx/controllers/profile.py @@ -0,0 +1,23 @@ +from owrx.controllers.admin import AdminController +from owrx.users import UserList, DefaultPasswordClass +from urllib.parse import parse_qs + + +class ProfileController(AdminController): + def isAuthorized(self): + return self.user is not None and self.user.is_enabled() and self.user.must_change_password + + def indexAction(self): + self.serve_template("pwchange.html", **self.template_variables()) + + def processPwChange(self): + data = parse_qs(self.get_body().decode("utf-8")) + data = {k: v[0] for k, v in data.items()} + userlist = UserList.getSharedInstance() + if "password" in data and "confirm" in data and data["password"] == data["confirm"]: + self.user.setPassword(DefaultPasswordClass(data["password"]), must_change_password=False) + userlist.store() + target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings" + else: + target = "/pwchange" + self.send_redirect(target) diff --git a/owrx/controllers/session.py b/owrx/controllers/session.py index 9b45d111..dc09820a 100644 --- a/owrx/controllers/session.py +++ b/owrx/controllers/session.py @@ -1,5 +1,5 @@ from .template import WebpageController -from urllib.parse import parse_qs +from urllib.parse import parse_qs, urlencode from uuid import uuid4 from http.cookies import SimpleCookie from owrx.users import UserList @@ -51,6 +51,8 @@ class SessionController(WebpageController): cookie = SimpleCookie() cookie["owrx-session"] = key target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings" + if user.must_change_password: + target = "/pwchange?{0}".format(urlencode({"ref": target})) self.send_redirect(target, cookies=cookie) return self.send_redirect("/login") diff --git a/owrx/http.py b/owrx/http.py index 5ced3090..1ccd216d 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -6,6 +6,7 @@ from owrx.controllers.api import ApiController from owrx.controllers.metrics import MetricsController from owrx.controllers.settings import SettingsController, GeneralSettingsController, SdrSettingsController from owrx.controllers.session import SessionController +from owrx.controllers.profile import ProfileController from http.server import BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs import re @@ -109,6 +110,8 @@ class Router(object): StaticRoute("/login", SessionController, options={"action": "loginAction"}), StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), StaticRoute("/logout", SessionController, options={"action": "logoutAction"}), + StaticRoute("/pwchange", ProfileController), + StaticRoute("/pwchange", ProfileController, method="POST", options={"action": "processPwChange"}), ] def find_route(self, request): diff --git a/owrx/users.py b/owrx/users.py index 38deebea..05857d13 100644 --- a/owrx/users.py +++ b/owrx/users.py @@ -56,7 +56,7 @@ class CleartextPassword(Password): class HashedPassword(Password): def __init__(self, pwinfo, algorithm="sha256"): self.iterations = 100000 - if (isinstance(pwinfo, str)): + if isinstance(pwinfo, str): self._createFromString(pwinfo, algorithm) else: self._loadFromDict(pwinfo) @@ -91,20 +91,30 @@ DefaultPasswordClass = HashedPassword class User(object): - def __init__(self, name: str, enabled: bool, password: Password): + def __init__(self, name: str, enabled: bool, password: Password, must_change_password: bool = False): self.name = name self.enabled = enabled self.password = password + self.must_change_password = must_change_password def toJson(self): return { "user": self.name, "enabled": self.enabled, + "must_change_password": self.must_change_password, "password": self.password.toJson() } - def setPassword(self, password: Password): + @staticmethod + def fromJson(d): + if "user" in d and "password" in d and "enabled" in d: + mcp = d["must_change_password"] if "must_change_password" in d else False + return User(d["user"], d["enabled"], Password.from_dict(d["password"]), mcp) + + def setPassword(self, password: Password, must_change_password: bool = None): self.password = password + if must_change_password is not None: + self.must_change_password = must_change_password def is_enabled(self): return self.enabled @@ -150,7 +160,7 @@ class UserList(object): with open(usersFile, "r") as f: users_json = json.load(f) - users = {u.name: u for u in [self._jsonToUser(d) for d in users_json]} + users = {u.name: u for u in [User.fromJson(d) for d in users_json]} self.file_modified = modified return users except FileNotFoundError: @@ -162,10 +172,6 @@ class UserList(object): logger.exception("error while processing users from %s", usersFile) return {} - def _jsonToUser(self, d): - if "user" in d and "password" in d and "enabled" in d: - return User(d["user"], d["enabled"], Password.from_dict(d["password"])) - def _userToJson(self, u): return u.toJson() diff --git a/owrxadmin/commands.py b/owrxadmin/commands.py index 115b34eb..46e52dc0 100644 --- a/owrxadmin/commands.py +++ b/owrxadmin/commands.py @@ -27,8 +27,8 @@ class UserCommand(Command, metaclass=ABCMeta): if args.noninteractive: print("Generating password for user {username}...".format(username=username)) password = self.getRandomPassword() + generated = True print('Password for {username} is "{password}".'.format(username=username, password=password)) - # TODO implement this threat print('This password is suitable for initial setup only, you will be asked to reset it on initial use.') print('This password cannot be recovered from the system, please copy it now.') else: @@ -37,7 +37,8 @@ class UserCommand(Command, metaclass=ABCMeta): if password != confirm: print("ERROR: Password mismatch.") sys.exit(1) - return password + generated = False + return password, generated def getRandomPassword(self, length=10): printable = list(string.ascii_letters) + list(string.digits) @@ -52,10 +53,10 @@ class NewUser(UserCommand): if username in userList: raise KeyError("User {username} already exists".format(username=username)) - password = self.getPassword(args, username) + password, generated = self.getPassword(args, username) print("Creating user {username}...".format(username=username)) - user = User(name=username, enabled=True, password=DefaultPasswordClass(password)) + user = User(name=username, enabled=True, password=DefaultPasswordClass(password), must_change_password=generated) userList.addUser(user) @@ -70,9 +71,9 @@ class DeleteUser(UserCommand): class ResetPassword(UserCommand): def run(self, args): username = self.getUser(args) - password = self.getPassword(args, username) + password, generated = self.getPassword(args, username) userList = UserList() - userList[username].setPassword(DefaultPasswordClass(password)) + userList[username].setPassword(DefaultPasswordClass(password), must_change_password=generated) # this is a change to an object in the list, not the list itself # in this case, store() is explicit userList.store()