Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user passwords (#301) #308

Merged
merged 62 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d0f2532
add password fields to schema
inventor02 Dec 23, 2024
44b77f7
add password related config options
inventor02 Dec 23, 2024
c4f274f
add ability to set password
inventor02 Dec 24, 2024
6b0870a
add hidden fields
inventor02 Dec 24, 2024
2b0ee54
improve docs
inventor02 Dec 24, 2024
457980b
password prompt at login
inventor02 Dec 24, 2024
3d94ae6
fix hidden fields
inventor02 Dec 24, 2024
150ffde
rearchitect login to use callbacks
inventor02 Dec 24, 2024
23fdd27
add password check to stockterminal
inventor02 Dec 24, 2024
216d0d6
refactor function nesting
inventor02 Dec 24, 2024
2b860cd
update token last login
inventor02 Dec 24, 2024
bdc6595
dodgy hack to only display the prompt once
inventor02 Dec 24, 2024
b4ee4a1
update tillweb
inventor02 Dec 24, 2024
3d9f39e
change configs to configitems
inventor02 Dec 24, 2024
b291a55
lint user.py
inventor02 Dec 24, 2024
0910216
lint ui.py
inventor02 Dec 24, 2024
f0d179c
lint stockterminal.py
inventor02 Dec 24, 2024
045fe1f
lint register.py
inventor02 Dec 24, 2024
800394e
handle hotkeypress() in password prompt
inventor02 Dec 24, 2024
61cc741
don't double-wrap users!
inventor02 Dec 24, 2024
ebe7afd
don't use nested functions here
inventor02 Dec 24, 2024
245f1bb
implement comparison operators for IntConfigItem
inventor02 Dec 24, 2024
392e4e9
change operators implemented
inventor02 Dec 24, 2024
f48c823
add dismissable param
inventor02 Dec 24, 2024
3e2aac7
force setting password on login if required
inventor02 Dec 24, 2024
db0e998
improve message formatting
inventor02 Dec 24, 2024
522dec8
change colour of info dialog
inventor02 Dec 24, 2024
2607636
change cleartext
inventor02 Dec 24, 2024
5228d4c
add password-only logon and unique password requirement
inventor02 Dec 24, 2024
4c9c450
lint user.py
inventor02 Dec 24, 2024
3b598cb
pass user to stockterminal
inventor02 Dec 24, 2024
d0ef394
fix config item handling (oops)
inventor02 Dec 24, 2024
361e218
dont allow multiple logon dialogs
inventor02 Dec 24, 2024
9c96d7d
dont force LOGON button
inventor02 Dec 24, 2024
7bfe11d
oops...
inventor02 Dec 24, 2024
9d534a8
update last seen when logging in with password
inventor02 Dec 24, 2024
4a9c151
Merge branch 'sde1000:main' into user-pins
inventor02 Jan 25, 2025
bef7418
show user ID in edit dialogs
inventor02 Jan 25, 2025
c19c7a3
change allow_password_login name and description
inventor02 Jan 25, 2025
282ade7
show ID in current user info
inventor02 Jan 25, 2025
5990bbf
get UID at login
inventor02 Jan 25, 2025
63d737a
clear on TAB
inventor02 Jan 25, 2025
4da665f
improve error message
inventor02 Jan 25, 2025
7d08f6b
switch to pbkdf2
inventor02 Jan 25, 2025
c0ec12b
clear last_successful_login when tokens are (re)assigned
inventor02 Jan 25, 2025
e8f9fc2
handle navigation away from UID
inventor02 Jan 25, 2025
efd588e
remove unique passwords config setting
inventor02 Jan 25, 2025
cbdb8d8
do not pass user to stockterminal
inventor02 Jan 26, 2025
508277d
use validate_positive_nonzero_int for UID
inventor02 Jan 26, 2025
63d51aa
refactor: move password hash and check into new module
inventor02 Jan 26, 2025
7c2338d
change token_password_timeout to interval and rename
inventor02 Jan 26, 2025
6453007
change sqlalchemy get to 2.0-compatible calls
inventor02 Jan 26, 2025
7236562
allow users to remove their own passwords
inventor02 Jan 26, 2025
6b78568
style: add newline at the end of passwords.py
inventor02 Jan 26, 2025
ad5f164
draw user ID as a string in the edit user dialog
inventor02 Jan 26, 2025
0c33542
Merge branch 'sde1000:main' into user-pins
inventor02 Jan 26, 2025
1f4b9b5
use last_successful_login instead of last_seen
inventor02 Jan 26, 2025
aa20ae4
use last_seen, but only set it after successful logins
inventor02 Jan 26, 2025
12703a9
fix config item name
inventor02 Jan 26, 2025
33fe7a7
refactor user.py
inventor02 Jan 27, 2025
cd739de
always allow clearing another user's password if you have permission
inventor02 Jan 28, 2025
7e2fea0
remove duplicate check
inventor02 Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions quicktill/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def __repr__(self):
keycode("K_PRICECHECK", "Price Check")
keycode("K_LOCK", "Lock")
keycode("K_ONLINE_ORDERS", "Online Orders")
keycode("K_PASS_LOGON", "Log On")

# Tendering keys referred to in the code
keycode("K_CASH", "Cash / Enter")
Expand Down
19 changes: 19 additions & 0 deletions quicktill/localutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
K_LOCK,
K_QUANTITY,
K_DRINKIN,
K_PASS_LOGON,
Key,
notekey,
paymentkey,
Expand Down Expand Up @@ -507,6 +508,7 @@ def global_hotkeys(register_hotkeys, stockterminal_location=["Bar"]):
K_STOCKTERMINAL: lambda: stockterminal.page(
register_hotkeys, stockterminal_location),
K_LOCK: lockscreen.lockpage,
K_PASS_LOGON: lambda: tillconfig.passlogon_handler(),
}


Expand All @@ -523,6 +525,13 @@ def activate_register_with_usertoken(register_hotkeys, timeout=300):
}


def activate_register_with_password(register_hotkeys, timeout=300):
return {
'passlogon_handler': lambda: register.handle_passlogon(
register_hotkeys, autolock=K_LOCK, timeout=timeout),
}


def activate_stockterminal_with_usertoken(
register_hotkeys,
stockterminal_location=["Bar"],
Expand All @@ -539,6 +548,16 @@ def activate_stockterminal_with_usertoken(
}


def activate_stockterminal_with_password(register_hotkeys,
stockterminal_location=["Bar"],
max_unattended_updates=5):
return {
'passlogon_handler': lambda: stockterminal.handle_passlogon(
register_hotkeys, stockterminal_location,
max_unattended_updates=max_unattended_updates),
}


class ServiceCharge(register.RegisterPlugin):
"""Apply a service charge to the current transaction

Expand Down
2 changes: 2 additions & 0 deletions quicktill/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,7 @@ class User(Base, Logged):
message = Column(String(), nullable=True,
doc="Message to present to user on their next keypress")
last_seen = Column(DateTime)
password = Column(String(), nullable=True)
groups = relationship("Group", secondary="group_grants", backref="users")
permissions = relationship(
"Permission",
Expand Down Expand Up @@ -932,6 +933,7 @@ class UserToken(Base, Logged):
description = Column(String())
user_id = Column('user', Integer, ForeignKey('users.id'))
last_seen = Column(DateTime)
last_successful_login = Column(DateTime, nullable=True)
user = relationship(User, backref='tokens')


Expand Down
49 changes: 49 additions & 0 deletions quicktill/passwords.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Till password hashing and checking logic.

This module provides functions to create 'password tuples' which take the format
`algorithm$iterations$salt$hash_hex`.
"""

import secrets
import hashlib


def compute_password_tuple(password):
"""Computes the password tuple for storage in the database.

Returns a string in the format `algorithm$iterations$salt$hash_hex`.
"""
iterations = 500_000
salt = secrets.token_hex(16)
hash = compute_pbkdf2(password, salt, iterations)
return f"pbkdf2${iterations}${salt}${hash}"


def compute_pbkdf2(value, salt, iterations):
"""Computes t he PBKDF2 hash for a value given a salt and number of
iterations.
"""
hash = hashlib.pbkdf2_hmac("sha256", bytes(value, "utf-8"),
bytes(salt, "utf-8"), iterations)
return hash.hex()


def check_password(password, tuple):
"""Checks a password against a tuple.

The tuple must be in the format `algorithm$iterations$salt$hash_hex`.
Malformed values will raise an exception.
"""
elems = tuple.split("$")
if len(elems) != 4:
raise Exception("Invalid password tuple presented (len(elems) != 4).")

algo = elems[0]
iterations = int(elems[1])
salt = elems[2]
hash = elems[3]

if algo == 'pbkdf2':
return compute_pbkdf2(password, salt, iterations) == hash
else:
raise Exception("Unsupported password algorithm: " + algo)
17 changes: 16 additions & 1 deletion quicktill/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -3423,7 +3423,22 @@ def handle_usertoken(t, *args, **kwargs):
Used in the configuration file to specify what happens when a user
token is handled by the default hotkey handler.
"""
u = user.user_from_token(t)
user.token_login(t,
lambda u: finalize_logon(u, *args, **kwargs))


def handle_passlogon(*args, **kwargs):
"""Password logon handler for the register.
"""
user.password_login(lambda u: finalize_logon(u, *args, **kwargs))


def finalize_logon(u, *args, **kwargs):
"""The other half of handle_usertoken.

This allows this function to be used as a callback, where, for example,
logging in requires some asynchronous user input (like a password).
"""
if u is None:
return
for p in ui.basicpage._pagelist:
Expand Down
18 changes: 14 additions & 4 deletions quicktill/stockterminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,22 @@ def set_location(self, location):


def handle_usertoken(t, *args, **kwargs):
"""
Called when a usertoken has been handled by the default hotkey
"""Called when a usertoken has been handled by the default hotkey
handler.
"""
user.token_login(t,
lambda u: finalize_logon(u, *args, **kwargs))


def handle_passlogon(*args, **kwargs):
"""Password logon handler for the register.
"""
user.password_login(lambda u: finalize_logon(u, *args, **kwargs))


def finalize_logon(u, *args, **kwargs):
"""a la register.finalize_handle_usertoken
"""
u = user.user_from_token(t)
if u is None:
return # Should already have toasted
return
return page(*args, user=u, **kwargs)
2 changes: 2 additions & 0 deletions quicktill/till.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,8 @@ def main():
tillconfig.usertoken_listen_v6 = val
elif opt == 'description':
tillconfig.configdescription = val
elif opt == 'passlogon_handler':
tillconfig.passlogon_handler = val
else:
log.warning("Unknown configuration option '%s'", opt)

Expand Down
5 changes: 5 additions & 0 deletions quicktill/tillconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ def usertoken_handler(t):
pass


# Called by UI code whenever the password logon key is pressed
def passlogon_handler():
pass


usertoken_listen = None
usertoken_listen_v6 = None

Expand Down
3 changes: 2 additions & 1 deletion quicktill/tillweb/templates/tillweb/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,13 @@
<th scope="col">Description</th>
<th scope="col">Value</th>
<th scope="col">Last used</th>
<th scope="col">Last successful login</th>
</tr>
</thead>
<tbody>
{% for t in tuser.tokens %}
<tr>
<td>{{t.description}}</td><td>{{t.token}}</td><td>{{t.last_seen|date:dtf}}</td>
<td>{{t.description}}</td><td>{{t.token}}</td><td>{{t.last_seen|date:dtf}}</td><td>{{t.last_successful_login|date:dtf}}</td>
</tr>
{% endfor %}
</tbody>
Expand Down
15 changes: 12 additions & 3 deletions quicktill/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1435,9 +1435,11 @@ class editfield(valuefield):
is passed the proposed new contents and the cursor index, and
should return either a (potentially updated) string or None if the
input is not allowed

hidden: mask value with * characters
"""
def __init__(self, y, x, w, keymap={}, f=None, flen=None, validate=None,
readonly=False):
readonly=False, hidden=False):
self.y = y
self.x = x
self.w = w
Expand All @@ -1450,6 +1452,7 @@ def __init__(self, y, x, w, keymap={}, f=None, flen=None, validate=None,
self.flen = flen
self.validate = validate
self.readonly = readonly
self.hidden = hidden
super().__init__(keymap, f)

# Internal attributes:
Expand Down Expand Up @@ -1487,8 +1490,14 @@ def draw(self):
self.i = self.c
self.win.clear(self.y, self.x, 1, self.w,
colour=self.win.colour.reversed)
self.win.addstr(self.y, self.x, self._f[self.i:self.i + self.w],
self.win.colour.reversed)

if self.hidden:
self.win.addstr(self.y, self.x, '*' * (self.c - self.i),
self.win.colour.reversed)
else:
self.win.addstr(self.y, self.x, self._f[self.i:self.i + self.w],
self.win.colour.reversed)

if self.focused:
self.win.move(self.y, self.x + self.c - self.i)
else:
Expand Down
Loading