Add hold-door web form with proper authorization #41

Open
mykhailo wants to merge 1 commits from mykhailo/inventory-app:issue-7-hold-door into master
3 changed files with 61 additions and 13 deletions

View File

@@ -8,7 +8,7 @@ from flask_wtf import FlaskForm
from oidc import login_required, read_user from oidc import login_required, read_user
from pymongo import MongoClient from pymongo import MongoClient
from wtforms import BooleanField, IntegerField, SelectField, StringField, validators from wtforms import BooleanField, IntegerField, SelectField, StringField, validators
from wtforms.validators import DataRequired from wtforms.validators import DataRequired, InputRequired
page_doorboy = Blueprint("doorboy", __name__) page_doorboy = Blueprint("doorboy", __name__)
db = MongoClient(const.MONGO_URI).get_default_database() db = MongoClient(const.MONGO_URI).get_default_database()
@@ -82,9 +82,19 @@ def save_doorboy_edit(token_id):
}) })
return redirect("/m/doorboy/me") return redirect("/m/doorboy/me")
def hold_duration_check(form, field):
# 0 cancels/closes an active hold immediately; any real hold must last at
# least the documented 5-second minimum (and at most 6 hours). Reject 1-4
# so a typo cannot arm an unusably short hold. field.data is None when the
# submitted value was not a valid integer; leave that to the other validators.
if field.data is None:
return
if field.data != 0 and not (5 <= field.data <= 21600):
raise validators.ValidationError("Duration must be 0 (to close now) or between 5 and 21600 seconds")
class HoldDoorForm(FlaskForm): class HoldDoorForm(FlaskForm):
door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor"]], validators=[DataRequired()]) door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor", "workshopdoor"]], validators=[DataRequired()])
duration = IntegerField('Duration in seconds', validators=[DataRequired(), validators.NumberRange(min=5, max=21600)]) duration = IntegerField('Duration in seconds', validators=[InputRequired(), hold_duration_check])
# duration=0 to override and close right away # duration=0 to override and close right away
@page_doorboy.route("/m/doorboy/hold", methods=["POST"]) @page_doorboy.route("/m/doorboy/hold", methods=["POST"])
@@ -92,16 +102,32 @@ class HoldDoorForm(FlaskForm):
def view_doorboy_hold(): def view_doorboy_hold():
user = read_user() user = read_user()
form = HoldDoorForm(request.form) form = HoldDoorForm(request.form)
if form.validate_on_submit(): if not form.validate_on_submit():
db.doorlog.insert_one({ # Validation or CSRF failure: report it instead of redirecting as if the
"method": "hold", # hold had been accepted, so the user knows the door will not be held.
"timestamp": datetime.utcnow(), errors = "; ".join(m for messages in form.errors.values() for m in messages)
"door": form.door_name.data, return "Invalid hold request: %s" % (errors or "bad or expired form"), 400
"approved": True,
"user": user["username"], door = form.door_name.data
"expires": datetime.utcnow() + timedelta(seconds=form.duration.data) if door == "workshopdoor":
}) access_group = "k-space:workshop"
return redirect("/m/doorboy") else:
access_group = "k-space:floor"
approved = access_group in g.users_lookup.get(user["username"], User()).groups
db.doorlog.insert_one({
"method": "hold",
"timestamp": datetime.utcnow(),
"door": door,
"approved": approved,
"user": user["username"],
"expires": datetime.utcnow() + timedelta(seconds=form.duration.data)
})
if approved:
return redirect("/m/doorboy")
# Logged for audit, but the proxy only honors approved holds, so tell the
# user the door will not actually be held rather than redirecting silently.
return "You are not in the access group required to hold this door", 403
# Writes open event to log, which is picked up by doorboy-proxy. # Writes open event to log, which is picked up by doorboy-proxy.
@page_doorboy.route("/m/doorboy/<door>/open") @page_doorboy.route("/m/doorboy/<door>/open")
@@ -135,6 +161,7 @@ def view_doorboy():
user = read_user() user = read_user()
workshop_access = "k-space:workshop" in g.users_lookup.get(user["username"], User()).groups workshop_access = "k-space:workshop" in g.users_lookup.get(user["username"], User()).groups
hold_form = HoldDoorForm()
latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10) latest_swipes = db.inventory.find({"component": "doorboy", "type":"token"}).sort([("last_seen", -1)]).limit(10)
return render_template("doorboy.html", **locals()) return render_template("doorboy.html", **locals())

View File

@@ -130,6 +130,11 @@ mongodb = mongoclient.get_default_database()
mongodb.inventory.create_index("shortener.slug", sparse=True, unique=True) mongodb.inventory.create_index("shortener.slug", sparse=True, unique=True)
mongodb.inventory.create_index("token.uid_hash", sparse=True, unique=True) mongodb.inventory.create_index("token.uid_hash", sparse=True, unique=True)
#mongodb.inventory.create_index("token.uid_hash", unique=True) #mongodb.inventory.create_index("token.uid_hash", unique=True)
# Supports doorboy-proxy's per-poll "newest active hold for this door" lookup
# (find_one({method,door,approved}, sort=timestamp desc)) without a collection scan.
# method is the leading equality field so the common no-active-hold poll does not
# scan every approved event for the door; timestamp trails for the descending sort.
mongodb.doorlog.create_index([("method", 1), ("door", 1), ("approved", 1), ("timestamp", -1)])
CATEGORY_COLORS = ( CATEGORY_COLORS = (
('membership-fee', '#acc236'), ('membership-fee', '#acc236'),

View File

@@ -37,6 +37,22 @@
</div> </div>
</div> </div>
<div>
<h3>Hold door open</h3>
<form method="POST" action="/m/doorboy/hold" autocomplete="off">
{{ hold_form.csrf_token }}
<select name="door_name">
<option value="grounddoor">Ground door</option>
<option value="frontdoor">Front door</option>
<option value="backdoor">Back door</option>
{% if workshop_access %}<option value="workshopdoor">Workshop</option>{% endif %}
</select>
<input type="number" name="duration" min="0" max="21600" value="300">
<button class="waves-effect waves-light btn" type="submit">Hold</button>
<p>Duration in seconds: 300 = 5 min, 3600 = 1h, max 21600 = 6h, 0 = cancel/close now.</p>
</form>
</div>
<hr> <hr>
<div><ul> <div><ul>
<li>Doors locations: <a href="https://k-space.ee/where">k-space.ee/where</a></li> <li>Doors locations: <a href="https://k-space.ee/where">k-space.ee/where</a></li>