Skip to content

Commit

Permalink
Merge branch 'MPSK_devices' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasjuhrich committed Oct 3, 2024
2 parents 1940cc7 + c350c1b commit 5948cfe
Show file tree
Hide file tree
Showing 22 changed files with 999 additions and 27 deletions.
91 changes: 91 additions & 0 deletions pycroft/lib/mpsk_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright (c) 2024. The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details
from sqlalchemy.orm import Session

from pycroft.model.mpsk_client import MPSKClient
from pycroft.model.user import User
from pycroft.lib.logging import log_user_event
from pycroft.helpers.i18n import deferred_gettext


def mpsk_delete(session: Session, *, mpsk_client: MPSKClient, processor: User) -> None:
message = deferred_gettext("Deleted mpsk client '{}'.").format(mpsk_client.name)
log_user_event(author=processor, user=mpsk_client.owner, message=message.to_json())

session.delete(mpsk_client)


def change_mac(session: Session, *, client: MPSKClient, mac: str, processor: User) -> MPSKClient:
"""
This method will change the mac address of the given mpsks client to the new
mac address.
:param session: session to use with the database.
:param client: the mpsks which should become a new mac address.
:param mac: the new mac address.
:param processor: the user who initiated the mac address change.
:return: the changed interface with the new mac address.
"""
old_mac = client.mac
client.mac = mac
message = deferred_gettext("Changed MAC address from {} to {}.").format(old_mac, mac)
if client.owner:
log_user_event(message.to_json(), processor, client.owner)
session.add(client)
return client


def mpsk_client_create(
session: Session, *, owner: User, name: str, mac: str, processor: User
) -> MPSKClient:
"""
creates a mpsks client for a given user with a mac address.
:param session: session to use with the database.
:param owner: the user who initiated the mac address change.
:param name: the name of the mpsks client.
:param mac: the new mac address.
:param processor: the user who initiated the mac address change.
"""
client = MPSKClient(name=name, owner_id=owner.id, mac=mac)

session.add(client)

message = deferred_gettext("Created MPSK Client '{name}' with MAC: {mac}.").format(
name=client.name,
mac=client.mac,
)

log_user_event(author=processor, user=owner, message=message.to_json())

return client


def mpsk_edit(
session: Session, *, client: MPSKClient, owner: User, name: str, mac: str, processor: User
) -> None:
if client.name != name:
message = deferred_gettext("Changed name of client '{}' to '{}'.").format(client.name, name)
client.name = name

log_user_event(author=processor, user=owner, message=message.to_json())

if client.owner_id != owner.id:
message = deferred_gettext("Transferred Host '{}' to {}.").format(client.name, owner.id)
log_user_event(author=processor, user=client.owner, message=message.to_json())

message = deferred_gettext("Transferred Host '{}' from {}.").format(
client.name, client.owner.id
)
log_user_event(author=processor, user=owner, message=message.to_json())

client.owner = owner

if client.mac != mac:
message = deferred_gettext("Changed MAC address of client '{}' to '{}'.").format(
client.name, mac
)
log_user_event(author=processor, user=owner, message=message.to_json())
client.mac = mac
session.add(client)
1 change: 1 addition & 0 deletions pycroft/model/_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .facilities import *
from .finance import *
from .host import *
from .mpsk_client import *
from .logging import *
from .net import *
from .port import *
Expand Down
36 changes: 36 additions & 0 deletions pycroft/model/alembic/versions/dda39ad43536_add_mpskclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Add MPSKClient
Revision ID: dda39ad43536
Revises: 5234d7ac2b4a
Create Date: 2024-09-28 10:01:39.952235
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "dda39ad43536"
down_revision = "5234d7ac2b4a"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"mpsk_client",
sa.Column("name", sa.String(), nullable=False),
sa.Column("owner_id", sa.Integer(), nullable=False),
sa.Column("mac", postgresql.types.MACADDR, nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(["owner_id"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("mac"),
)
op.create_index(op.f("ix_mpsk_client_owner_id"), "mpsk_client", ["owner_id"], unique=False)


def downgrade():
op.drop_index(op.f("ix_mpsk_client_owner_id"), table_name="mpsk_client")
op.drop_table("mpsk_client")
44 changes: 44 additions & 0 deletions pycroft/model/mpsk_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (c) 2024. The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details

from __future__ import annotations

from packaging.utils import InvalidName
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, validates, Mapped, mapped_column
from sqlalchemy.types import String

from pycroft.helpers.net import mac_regex
from pycroft.model.base import IntegerIdModel
from pycroft.model.host import MulticastFlagException
from pycroft.model.type_aliases import mac_address
from pycroft.model.types import InvalidMACAddressException
from pycroft.model.user import User


class MPSKClient(IntegerIdModel):

name: Mapped[str] = mapped_column(String, nullable=False)

owner_id: Mapped[int | None] = mapped_column(
ForeignKey(User.id, ondelete="CASCADE"), index=True, nullable=False
)
owner: Mapped[User] = relationship(User, back_populates="mpsk_clients")
mac: Mapped[mac_address] = mapped_column(unique=True)

@validates("mac")
def validate_mac(self, _, mac_address):
match = mac_regex.match(mac_address)
if not match:
raise InvalidMACAddressException(f"MAC address {mac_address!r} is not valid")
if int(mac_address[0:2], base=16) & 1:
raise MulticastFlagException("Multicast bit set in MAC address")
return mac_address

@validates("name")
def validate_name(self, _, name):
if name.strip() == "":
raise InvalidName("Name cannot be empty")

return name
14 changes: 10 additions & 4 deletions pycroft/model/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
# Backrefs
from .logging import LogEntry, UserLogEntry, TaskLogEntry
from .host import Host
from .mpsk_client import MPSKClient
from .swdd import Tenancy
from .task import UserTask
from .traffic import TrafficVolume
Expand Down Expand Up @@ -252,6 +253,11 @@ class User(BaseUser, UserMixin):
hosts: Mapped[list[Host]] = relationship(
back_populates="owner", cascade="all, delete-orphan"
)

mpsk_clients: Mapped[list[MPSKClient]] = relationship(
back_populates="owner", cascade="all, delete-orphan"
)

authored_log_entries: Mapped[list[LogEntry]] = relationship(
back_populates="author", viewonly=True
)
Expand Down Expand Up @@ -368,15 +374,15 @@ def latest_log_entry(self) -> UserLogEntry | None:
return max(le, key=operator.attrgetter("created_at"))

@property
def wifi_password(self):
"""Store a hash of a given plaintext passwd for the user.
def wifi_password(self) -> str | None:
"""return the cleartext wifi password (without crypt prefix) if available.
:returns: `None` if the `wifi_passwd_hash` is not set or is not cleartext.
"""

if self.wifi_passwd_hash is not None and self.wifi_passwd_hash.startswith(clear_password_prefix):
return self.wifi_passwd_hash.replace(clear_password_prefix, '', 1)

raise ValueError("Cleartext password not available.")
return None

@wifi_password.setter
def wifi_password(self, value):
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ module = [
"pycroft.lib.user.*",
"web.blueprints.finance",
"web.blueprints.finance.*",
"pycroft.lib.mpsk_client",
]
strict_optional = true

Expand Down
1 change: 1 addition & 0 deletions tests/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
SwitchFactory,
SwitchPortFactory,
)
from .mpsk import MPSKFactory, BareMPSKFactory
from .net import SubnetFactory, VLANFactory
from .property import (
PropertyGroupFactory,
Expand Down
26 changes: 26 additions & 0 deletions tests/factories/mpsk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (c) 2024. The Pycroft Authors. See the AUTHORS file.
# This file is part of the Pycroft project and licensed under the terms of
# the Apache License, Version 2.0. See the LICENSE file for details
import factory
from tests.factories.base import BaseFactory
from pycroft.model.mpsk_client import MPSKClient

from tests.factories.user import UserFactory
from tests.factories.host import UnicastMacProvider

factory.Faker.add_provider(UnicastMacProvider)


class BareMPSKFactory(BaseFactory):
"""A host without owner or interface."""

class Meta:
model = MPSKClient

mac = factory.Faker("unicast_mac_address")
name = factory.Faker("name")


class MPSKFactory(BareMPSKFactory):

owner = factory.SubFactory(UserFactory)
1 change: 1 addition & 0 deletions tests/factories/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Meta:
registered_at = Faker('date_time')
password = None
passwd_hash = PASSWORD
wifi_passwd_hash = "{clear}password"
email = Faker('email')
account = factory.SubFactory(AccountFactory, type="USER_ASSET")
room = factory.SubFactory(RoomFactory)
Expand Down
29 changes: 29 additions & 0 deletions tests/frontend/api/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from tests.frontend.assertions import TestClient


@pytest.fixture(scope="module", autouse=True)
def client(module_test_client: TestClient) -> TestClient:
return module_test_client


@pytest.fixture(scope="module", autouse=True)
def api_key(app) -> str:
api_key = "secrettestapikey"
app.config["PYCROFT_API_KEY"] = api_key
return api_key


@pytest.fixture(scope="module", autouse=True)
def max_clients(app) -> int:
max_clients = 5
app.config["MAX_MPSKS"] = max_clients
return max_clients


# TODO put this into the client
@pytest.fixture(scope="module")
def auth_header(api_key) -> dict[str, str]:
# see `api.v0.parse_authorization_header`
return {"AUTHORIZATION": f"apikey {api_key}"}
Loading

0 comments on commit 5948cfe

Please sign in to comment.