Skip to content

Commit

Permalink
Merge pull request #101 from MakerSpaceLeiden/master
Browse files Browse the repository at this point in the history
Merge current changes into prod
  • Loading branch information
dirkx authored Oct 29, 2024
2 parents 98e75e1 + 24793c8 commit 9e6e76b
Show file tree
Hide file tree
Showing 77 changed files with 1,240 additions and 459 deletions.
30 changes: 30 additions & 0 deletions acl/management/commands/acl_show.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import sys

from django.core.management.base import BaseCommand

from acl.models import Machine, useNeedsToStateStr
from members.models import User


class Command(BaseCommand):
help = "Export binary tag/member file for a given machine."

def add_arguments(self, parser):
parser.add_argument("machine", type=str, help="Machine this list is for")
parser.add_argument("user", type=str, help="User that list is for")

def handle(self, *args, **options):
rc = 0

machine = options["machine"]
user = options["user"]

machine = Machine.objects.get(node_machine_name=machine)
user = User.objects.get(last_name=user)

(needs, has) = machine.useState(user)
res = needs & has
print(f"has({has:X}) & needs({needs:X}) = {res:X}")
print(useNeedsToStateStr(needs, has))

sys.exit(rc)
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@


class Migration(migrations.Migration):

dependencies = [
('acl', '0010_changetracker'),
("acl", "0010_changetracker"),
]

operations = [
migrations.AlterModelOptions(
name='location',
options={'ordering': ['name']},
name="location",
options={"ordering": ["name"]},
),
migrations.AlterModelOptions(
name='machine',
options={'ordering': ['name']},
name="machine",
options={"ordering": ["name"]},
),
migrations.AlterModelOptions(
name='permittype',
options={'ordering': ['name']},
name="permittype",
options={"ordering": ["name"]},
),
]
48 changes: 42 additions & 6 deletions acl/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.utils import timezone
from simple_history.models import HistoricalRecords

# from pettycash.models import PettycashBalanceCache
from members.models import Tag, User

logger = logging.getLogger(__name__)
Expand All @@ -31,15 +32,21 @@ class MachineUseFlags(IntEnum):
PERMIT = 2
FORM = 4
APPROVE = 8
INSTRUCT = 16
BUDGET = 32
OVERRIDE = 64


def bits2str(needs, has):
if needs:
if has:
return "ok"
return "yes"
else:
return "fail"
return "nn"
else:
if has:
return "NN"
return "n/a"


def useNeedsToStateStr(needs, has):
Expand All @@ -55,6 +62,18 @@ def useNeedsToStateStr(needs, has):
out += ", approve:" + bits2str(
needs & MachineUseFlags.APPROVE, has & MachineUseFlags.APPROVE
)

if has & MachineUseFlags.INSTRUCT:
out += ", instructor=yes"
else:
out += ", instructor=no"
if has & MachineUseFlags.BUDGET:
out += ", budget=sufficient"
else:
out += ", budget=no"
out += ", override:" + bits2str(
needs & MachineUseFlags.OVERRIDE, has & MachineUseFlags.OVERRIDE
)
out += " = "
if has & needs == needs:
out += "ok"
Expand Down Expand Up @@ -91,7 +110,8 @@ def hasThisPermit(self, user):
return False

class Meta:
ordering = ['name']
ordering = ["name"]


class Location(models.Model):
name = models.CharField(max_length=40, unique=True)
Expand All @@ -102,7 +122,8 @@ def __str__(self):
return self.name

class Meta:
ordering = ['name']
ordering = ["name"]


class NodeField(models.CharField):
def get_prep_value(self, value):
Expand Down Expand Up @@ -178,6 +199,8 @@ def useState(self, user):
needs |= MachineUseFlags.FORM
if self.requires_permit and self.requires_permit.require_ok_trustee:
needs |= MachineUseFlags.APPROVE
if self.out_of_order:
needs |= MachineUseFlags.OVERRIDE

flags = 0
if user.is_active:
Expand All @@ -188,6 +211,19 @@ def useState(self, user):
flags |= MachineUseFlags.PERMIT
if e and e.active:
flags |= MachineUseFlags.APPROVE
if self.canInstruct(user):
flags |= MachineUseFlags.INSTRUCT
if user.pettycash_cache.first().balance > settings.MIN_BALANCE_FOR_CREDIT:
flags |= MachineUseFlags.BUDGET

# Normal users can only operate machines that are unlocked.
# We may allow admins/some group to also operate unsafe
# machines by doing something special here. So hence
# we do not set the OVERRIDE bit here.
#
if user.admin or self.canInstruct(user):
flags |= MachineUseFlags.OVERRIDE

return [needs, flags]

def canOperate(self, user):
Expand All @@ -209,7 +245,8 @@ def canInstruct(self, user):
return self.requires_permit.permit.hasThisPermit(user)

class Meta:
ordering = ['name']
ordering = ["name"]


# Special sort of create/get - where we ignore the issuer when looking for it.
# but add it in if we're creating it for the first time.
Expand Down Expand Up @@ -398,7 +435,6 @@ def save(self, *args, **kwargs):
return super(Entitlement, self).save(*args, **kwargs)



class RecentUse(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
machine = models.ForeignKey(Machine, on_delete=models.CASCADE)
Expand Down
7 changes: 4 additions & 3 deletions acl/templates/acl/member_overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ <h2 class="card-title">Personal Details</h2>
{% endif %}
</dl>
<div>
{% if user.is_privileged or member == user %}
<button type="button" class="btn btn-primary btn-primary-custom" onclick="window.location.href='{% url 'userdetails' %}?redirect_to=personal_page'">Edit</button>
{% if member == user %}
<button type="button" class="btn btn-primary btn-primary-custom" onclick="window.location.href='{% url 'userdetails' %}?redirect_to=personal_page'">Edit</button>
{% endif %}
{% if user.is_privileged %}
<button type="button" class="btn btn-danger" onclick="window.location.href='{% url 'userdelete' member.id %}'">Delete</button>
<button type="button" class="btn btn-primary" onclick="window.location.href='{% url 'userdetails_admin_edit' member.id %}'">Edit</button>
<button type="button" class="btn btn-danger" onclick="window.location.href='{% url 'userdelete' member.id %}'">Delete</button>
{% endif %}
</div>
</div>
Expand Down
13 changes: 9 additions & 4 deletions acl/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
#
path("acl/api/v1/getok/<str:machine>", views.api_getok, name="acl-v1-getok"),
path(
"acl/api/v1/getok4node/<str:node>",
views.api_getok_by_node,
name="acl-v1-getok4-node",
),
"acl/api/v1/getok4node/<str:node>",
views.api_getok_by_node,
name="acl-v1-getok4-node",
),
# Provide metadata on a tag, requires a valid tag and a bearer token.
#
# path("acl/api/v1/gettaginfo", views.api_gettaginfo, name="acl-v1-gettaginfo"),
Expand Down Expand Up @@ -84,4 +84,9 @@
views.api_gettags4machineBIN,
name="acl-v1-gettags-bin",
),
path(
"acl/api/v2/gettags4machineBIN/<str:machine>",
views.api2_gettags4machineBIN,
name="acl-v2-gettags-bin",
),
]
67 changes: 58 additions & 9 deletions acl/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import logging
import re
import secrets
from functools import wraps

Expand Down Expand Up @@ -614,7 +615,7 @@ def api_gettags4machine(request, terminal=None, machine=None):
try:
machine = Machine.objects.get(name=machine)
except ObjectDoesNotExist:
logger.error(f"Machine {machine} not found, denied.")
logger.error(f"get4machine: Machine '{machine}' not found, denied.")
return HttpResponse("Machine not found", status=404, content_type="text/plain")

out = []
Expand Down Expand Up @@ -679,6 +680,21 @@ def byte_xor(ba1, ba2):
return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)])


def nameShorten(name, maxlen=10):
if len(name) <= maxlen:
return name
# parts = name.split(' ')
parts = re.split("[^a-zA-Z]", name)
name = parts.pop(0)
for i in parts:
if len(name) > maxlen - 1:
return name[0:maxlen]
if len(name) + len(i) > maxlen:
i = i[0]
name += i
return name


# Note: we are not checking if this terminal is actually associated
# with this node or machine. I.e any valid terminal can ask
# anything about the others. We may not want that in the future.
Expand Down Expand Up @@ -707,11 +723,18 @@ def byte_xor(ba1, ba2):
#
# 116+LT+LM EOF
#
def tags4machineBIN(terminal=None, machine=None):
# MSL2 -- same as above; but the AES block no longer just contains
# the name of the user; but also their unqiue user ID (for API
# purposes) and their first/last name separate.
#
def tags4machineBIN(terminal=None, machine=None, v2=False):
try:
machine = Machine.objects.get(name=machine)
machine = Machine.objects.get(node_machine_name=machine)
ctc = change_tracker_counter()
except ObjectDoesNotExist:
logger.error(
f"BIN request for an unknown machine: {machine} (node-machine-name)"
)
raise ObjectDoesNotExist

tl = []
Expand All @@ -727,6 +750,15 @@ def tags4machineBIN(terminal=None, machine=None):
key = secrets.token_bytes(32)
uiv = hashlib.sha256(iv + udx.to_bytes(4, "big")).digest()[0:16]

name = user.name()
block = b""
if v2:
# The identifier is treated like an opaque string; i.e. it may well be a UUID, etc.
block += str(user.id).encode("ASCII") + b"\0"
# shorter, simplified name for very small display purposes.
block += nameShorten(user.first_name.encode("ASCII"), 12) + b"\0"
block += name.encode("utf-8")

# This is a weak AES mode; with no protection against
# bit flipping, clear text, etc. However it is integrity
# protected during transport; and only protects a name
Expand All @@ -736,10 +768,9 @@ def tags4machineBIN(terminal=None, machine=None):
# modern mode such as CGM (which # is not supported yet
# by ESP32 anyway).
#
name = user.name()
clr = pad(name.encode("utf-8"), AES.block_size)
clr = pad(block, AES.block_size)
if len(clr) > 128:
raise Exception("name too large")
raise Exception("information block too large")

enc = AES.new(key, AES.MODE_CBC, iv=uiv).encrypt(clr)

Expand Down Expand Up @@ -779,7 +810,10 @@ def tags4machineBIN(terminal=None, machine=None):
tlb += e["udx"].to_bytes(4, "big")

hdr = b""
hdr += "MSL1".encode("ASCII")
if v2:
hdr += "MSL2".encode("ASCII")
else:
hdr += "MSL1".encode("ASCII")
hdr += ctc.count.to_bytes(
4, "big"
) # byte order not strictly needed - opaque 4 bytes.
Expand All @@ -802,7 +836,22 @@ def api_gettags4machineBIN(request, terminal=None, machine=None):
try:
out = tags4machineBIN(terminal, machine)
except ObjectDoesNotExist:
logger.error(f"Machine {machine} not found, denied.")
logger.error(f"getBIN: Machine '{machine}' not found, denied.")
return HttpResponse("Machine not found", status=404, content_type="text/plain")
except Exception as e:
logger.error(f"Exception: {e}")
return HttpResponse("Internal Error", status=500, content_type="text/plain")

return HttpResponse(out, status=200, content_type="application/octet-stream")


@csrf_exempt
@is_paired_terminal
def api2_gettags4machineBIN(request, terminal=None, machine=None):
try:
out = tags4machineBIN(terminal, machine, v2=True)
except ObjectDoesNotExist:
logger.error(f"getBIN: Machine '{machine}' not found, denied.")
return HttpResponse("Machine not found", status=404, content_type="text/plain")
except Exception as e:
logger.error(f"Exception: {e}")
Expand All @@ -818,7 +867,7 @@ def api_getok(request, machine=None, tag=None):
try:
machine = Machine.objects.get(node_machine_name=machine)
except ObjectDoesNotExist:
logger.error("Machine '{}' not found, denied.".format(machine))
logger.error("getok: Machine '{}' not found, denied.".format(machine))
return HttpResponse("Machine not found", status=404, content_type="text/plain")
try:
r = RecentUse(user=tag.owner, machine=machine)
Expand Down
Loading

0 comments on commit 9e6e76b

Please sign in to comment.