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

Mpsk devices #749

Merged
merged 50 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
72c2731
added frontend MPSK Clients
agmes4 Sep 14, 2024
4e06e42
added MSPK Client Model
agmes4 Sep 15, 2024
6363c73
changed name to mspk client module
agmes4 Sep 15, 2024
eaf6a56
changes to model
agmes4 Sep 15, 2024
24eedaa
refactoring
agmes4 Sep 16, 2024
3f56a87
added api mpsk
agmes4 Sep 16, 2024
bcf8a0d
renaming for matching
agmes4 Sep 16, 2024
adc3ce8
added naming constraint
agmes4 Sep 17, 2024
a784c5e
added model test for mpsk
agmes4 Sep 17, 2024
cbcf2c4
Update pycroft/model/mpsk_client.py
agmes4 Sep 18, 2024
f7df0d3
Update pycroft/model/mpsk_client.py
agmes4 Sep 18, 2024
6e9c980
Update web/api/v0/__init__.py
agmes4 Sep 18, 2024
c0093d1
fixed suggestion
agmes4 Sep 18, 2024
65ca87f
Update web/api/v0/__init__.py
agmes4 Sep 18, 2024
77bdef1
Update web/blueprints/mpskclient/__init__.py
agmes4 Sep 18, 2024
a7c17e3
Update tests/model/test_mpsk.py
agmes4 Sep 18, 2024
9eefa0a
removed decorator
agmes4 Sep 18, 2024
d1689b2
refactoring test
agmes4 Sep 18, 2024
dc42b96
fixed API mpsk delete
agmes4 Sep 18, 2024
ec21f15
added error when exceeding max amount MPSKS
agmes4 Sep 19, 2024
f30891d
checking for naming error and amount exceeded
agmes4 Sep 19, 2024
b60a881
added test for exceeded clients
agmes4 Sep 19, 2024
9475c51
added key arg creat mpsks client
agmes4 Sep 19, 2024
6cff7d1
Test MPSK: added test exceed admin
agmes4 Sep 19, 2024
ab23c44
changed Create MPSK
agmes4 Sep 19, 2024
3cafd5c
api mpsk api more clients
agmes4 Sep 19, 2024
cda3369
Update web/api/v0/__init__.py
agmes4 Sep 19, 2024
22dc893
fixed typo
agmes4 Sep 19, 2024
99c47af
lib mpsks added session as parameter
agmes4 Sep 21, 2024
26ed413
MPSKS Lib: added type hint session
agmes4 Sep 25, 2024
8f7f359
fixed wrong type in passings
agmes4 Sep 25, 2024
85febde
schema: add `mpsk_client`
lukasjuhrich Sep 28, 2024
10002d0
Remove now unused AmountExceededError
lukasjuhrich Sep 28, 2024
62902d2
Fix error reporting for frontend tests
lukasjuhrich Sep 28, 2024
c9148d2
api: Make certain error messages more specific
lukasjuhrich Sep 28, 2024
a52ec26
tests: POC API test for `add-mpsk`
lukasjuhrich Sep 28, 2024
97373c6
lib.mpsk_client: Enforce keyword-only arguments
lukasjuhrich Sep 28, 2024
4fc6e29
model: Allow accessing `user.wifi_password` even if stored in crypt
lukasjuhrich Sep 28, 2024
3b0b18c
model: rename backref `User.mpsks` → `User.mpsk_clients`
lukasjuhrich Sep 28, 2024
f5dc3ff
tests: move `test_api` to `api.test_mpsk` and introduce class
lukasjuhrich Sep 28, 2024
9c9ad26
added api tests
agmes4 Sep 29, 2024
a26a496
refactoring and renaming of error codes
agmes4 Sep 29, 2024
0f85476
added test for API
agmes4 Sep 29, 2024
886e4b3
API: added get mpsks clients
agmes4 Sep 29, 2024
e4dec3a
added mpsks change tests
agmes4 Sep 29, 2024
da89cae
API: renaming of error codes MPSK
agmes4 Sep 29, 2024
139778a
API: refactoring
agmes4 Sep 29, 2024
489c55f
fixed added admin mpsk test
agmes4 Sep 29, 2024
7534a7d
added lib.mpsk_clients pyproject.toml
agmes4 Sep 30, 2024
c350c1b
Change mpsk icon to `wifi`
lukasjuhrich Oct 3, 2024
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
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
Loading