implement forced password change for generated passwords
This commit is contained in:
parent
ed6594401c
commit
331e9627d6
32
htdocs/pwchange.html
Normal file
32
htdocs/pwchange.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OpenWebRX Password change</title>
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="static/css/login.css" />
|
||||||
|
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||||
|
<script src="static/lib/Header.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${header}
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login">
|
||||||
|
<div class="alert alert-primary">
|
||||||
|
Your password has been automatically generated and must be changed in order to proceed.
|
||||||
|
</div>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm">Password confirmation</label>
|
||||||
|
<input type="password" class="form-control" id="confirm" name="confirm" placeholder="Password confirmation">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-login">Change password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
@ -10,33 +10,36 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Authentication(object):
|
class Authentication(object):
|
||||||
def isAuthenticated(self, request):
|
def getUser(self, request):
|
||||||
if "owrx-session" not in request.cookies:
|
if "owrx-session" not in request.cookies:
|
||||||
return False
|
return None
|
||||||
session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value)
|
session = SessionStorage.getSharedInstance().getSession(request.cookies["owrx-session"].value)
|
||||||
if session is None:
|
if session is None:
|
||||||
return False
|
return None
|
||||||
if "user" not in session:
|
if "user" not in session:
|
||||||
return False
|
return None
|
||||||
userList = UserList.getSharedInstance()
|
userList = UserList.getSharedInstance()
|
||||||
try:
|
try:
|
||||||
user = userList[session["user"]]
|
return userList[session["user"]]
|
||||||
return user.is_enabled()
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
class AdminController(WebpageController):
|
class AdminController(WebpageController):
|
||||||
def __init__(self, handler, request, options):
|
def __init__(self, handler, request, options):
|
||||||
self.authentication = Authentication()
|
self.authentication = Authentication()
|
||||||
|
self.user = self.authentication.getUser(request)
|
||||||
super().__init__(handler, request, options)
|
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):
|
def handle_request(self):
|
||||||
config = Config.get()
|
config = Config.get()
|
||||||
if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
|
if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
|
||||||
self.send_response("Web Admin is disabled", code=403)
|
self.send_response("Web Admin is disabled", code=403)
|
||||||
return
|
return
|
||||||
if self.authentication.isAuthenticated(self.request):
|
if self.isAuthorized():
|
||||||
super().handle_request()
|
super().handle_request()
|
||||||
else:
|
else:
|
||||||
target = "/login?{0}".format(parse.urlencode({"ref": self.request.path}))
|
target = "/login?{0}".format(parse.urlencode({"ref": self.request.path}))
|
||||||
|
23
owrx/controllers/profile.py
Normal file
23
owrx/controllers/profile.py
Normal file
@ -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)
|
@ -1,5 +1,5 @@
|
|||||||
from .template import WebpageController
|
from .template import WebpageController
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs, urlencode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
from owrx.users import UserList
|
from owrx.users import UserList
|
||||||
@ -51,6 +51,8 @@ class SessionController(WebpageController):
|
|||||||
cookie = SimpleCookie()
|
cookie = SimpleCookie()
|
||||||
cookie["owrx-session"] = key
|
cookie["owrx-session"] = key
|
||||||
target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings"
|
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)
|
self.send_redirect(target, cookies=cookie)
|
||||||
return
|
return
|
||||||
self.send_redirect("/login")
|
self.send_redirect("/login")
|
||||||
|
@ -6,6 +6,7 @@ from owrx.controllers.api import ApiController
|
|||||||
from owrx.controllers.metrics import MetricsController
|
from owrx.controllers.metrics import MetricsController
|
||||||
from owrx.controllers.settings import SettingsController, GeneralSettingsController, SdrSettingsController
|
from owrx.controllers.settings import SettingsController, GeneralSettingsController, SdrSettingsController
|
||||||
from owrx.controllers.session import SessionController
|
from owrx.controllers.session import SessionController
|
||||||
|
from owrx.controllers.profile import ProfileController
|
||||||
from http.server import BaseHTTPRequestHandler
|
from http.server import BaseHTTPRequestHandler
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
import re
|
import re
|
||||||
@ -109,6 +110,8 @@ class Router(object):
|
|||||||
StaticRoute("/login", SessionController, options={"action": "loginAction"}),
|
StaticRoute("/login", SessionController, options={"action": "loginAction"}),
|
||||||
StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}),
|
StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}),
|
||||||
StaticRoute("/logout", SessionController, options={"action": "logoutAction"}),
|
StaticRoute("/logout", SessionController, options={"action": "logoutAction"}),
|
||||||
|
StaticRoute("/pwchange", ProfileController),
|
||||||
|
StaticRoute("/pwchange", ProfileController, method="POST", options={"action": "processPwChange"}),
|
||||||
]
|
]
|
||||||
|
|
||||||
def find_route(self, request):
|
def find_route(self, request):
|
||||||
|
@ -56,7 +56,7 @@ class CleartextPassword(Password):
|
|||||||
class HashedPassword(Password):
|
class HashedPassword(Password):
|
||||||
def __init__(self, pwinfo, algorithm="sha256"):
|
def __init__(self, pwinfo, algorithm="sha256"):
|
||||||
self.iterations = 100000
|
self.iterations = 100000
|
||||||
if (isinstance(pwinfo, str)):
|
if isinstance(pwinfo, str):
|
||||||
self._createFromString(pwinfo, algorithm)
|
self._createFromString(pwinfo, algorithm)
|
||||||
else:
|
else:
|
||||||
self._loadFromDict(pwinfo)
|
self._loadFromDict(pwinfo)
|
||||||
@ -91,20 +91,30 @@ DefaultPasswordClass = HashedPassword
|
|||||||
|
|
||||||
|
|
||||||
class User(object):
|
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.name = name
|
||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.must_change_password = must_change_password
|
||||||
|
|
||||||
def toJson(self):
|
def toJson(self):
|
||||||
return {
|
return {
|
||||||
"user": self.name,
|
"user": self.name,
|
||||||
"enabled": self.enabled,
|
"enabled": self.enabled,
|
||||||
|
"must_change_password": self.must_change_password,
|
||||||
"password": self.password.toJson()
|
"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
|
self.password = password
|
||||||
|
if must_change_password is not None:
|
||||||
|
self.must_change_password = must_change_password
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return self.enabled
|
return self.enabled
|
||||||
@ -150,7 +160,7 @@ class UserList(object):
|
|||||||
with open(usersFile, "r") as f:
|
with open(usersFile, "r") as f:
|
||||||
users_json = json.load(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
|
self.file_modified = modified
|
||||||
return users
|
return users
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@ -162,10 +172,6 @@ class UserList(object):
|
|||||||
logger.exception("error while processing users from %s", usersFile)
|
logger.exception("error while processing users from %s", usersFile)
|
||||||
return {}
|
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):
|
def _userToJson(self, u):
|
||||||
return u.toJson()
|
return u.toJson()
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ class UserCommand(Command, metaclass=ABCMeta):
|
|||||||
if args.noninteractive:
|
if args.noninteractive:
|
||||||
print("Generating password for user {username}...".format(username=username))
|
print("Generating password for user {username}...".format(username=username))
|
||||||
password = self.getRandomPassword()
|
password = self.getRandomPassword()
|
||||||
|
generated = True
|
||||||
print('Password for {username} is "{password}".'.format(username=username, password=password))
|
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 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.')
|
print('This password cannot be recovered from the system, please copy it now.')
|
||||||
else:
|
else:
|
||||||
@ -37,7 +37,8 @@ class UserCommand(Command, metaclass=ABCMeta):
|
|||||||
if password != confirm:
|
if password != confirm:
|
||||||
print("ERROR: Password mismatch.")
|
print("ERROR: Password mismatch.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return password
|
generated = False
|
||||||
|
return password, generated
|
||||||
|
|
||||||
def getRandomPassword(self, length=10):
|
def getRandomPassword(self, length=10):
|
||||||
printable = list(string.ascii_letters) + list(string.digits)
|
printable = list(string.ascii_letters) + list(string.digits)
|
||||||
@ -52,10 +53,10 @@ class NewUser(UserCommand):
|
|||||||
if username in userList:
|
if username in userList:
|
||||||
raise KeyError("User {username} already exists".format(username=username))
|
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))
|
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)
|
userList.addUser(user)
|
||||||
|
|
||||||
|
|
||||||
@ -70,9 +71,9 @@ class DeleteUser(UserCommand):
|
|||||||
class ResetPassword(UserCommand):
|
class ResetPassword(UserCommand):
|
||||||
def run(self, args):
|
def run(self, args):
|
||||||
username = self.getUser(args)
|
username = self.getUser(args)
|
||||||
password = self.getPassword(args, username)
|
password, generated = self.getPassword(args, username)
|
||||||
userList = UserList()
|
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
|
# this is a change to an object in the list, not the list itself
|
||||||
# in this case, store() is explicit
|
# in this case, store() is explicit
|
||||||
userList.store()
|
userList.store()
|
||||||
|
Loading…
Reference in New Issue
Block a user