diff --git a/htdocs/pwchange.html b/htdocs/pwchange.html
new file mode 100644
index 0000000..e3c433a
--- /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 5ce4a06..822bf63 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 0000000..6542553
--- /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 9b45d11..dc09820 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 5ced309..1ccd216 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 38deebe..05857d1 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 115b34e..46e52dc 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()