From d013793af70f726abb24acf3c88e05c1b19fcdb6 Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 25 Oct 2023 20:01:59 +0200 Subject: [PATCH 1/9] add group mapping and blocking flag to `radius_property` table --- pycroft/model/hades.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/pycroft/model/hades.py b/pycroft/model/hades.py index db78d00a9..373120b7c 100644 --- a/pycroft/model/hades.py +++ b/pycroft/model/hades.py @@ -2,8 +2,20 @@ pycroft.model.hades ~~~~~~~~~~~~~~~~~~~ """ -from sqlalchemy import literal, Column, String, func, union_all, Table, Integer, \ - PrimaryKeyConstraint, null, and_ +from sqlalchemy import ( + literal, + Column, + String, + func, + union_all, + Table, + Integer, + PrimaryKeyConstraint, + null, + and_, + Boolean, + select, +) from sqlalchemy.orm import Query, aliased, configure_mappers from pycroft.model.base import ModelBase @@ -33,6 +45,8 @@ 'radius_property', ModelBase.metadata, Column('property', String, primary_key=True), + Column("hades_group_name", String, nullable=False), + Column("is_blocking_group", Boolean, nullable=False), ) # This is a hack to enforce that Views are created after _all_ their # depenencies. The Views' creation is then targeted after @@ -212,19 +226,19 @@ literal('Yes').label('Value'), ]), # Egress-VLAN-Name := 2hades-unauth, blocking groups - Query([ - radius_property.c.property.label('GroupName'), - literal("Egress-VLAN-Name").label('Attribute'), - literal(":=").label('Op'), - literal("2hades-unauth").label('Value'), - ]), + select( + radius_property.c.hades_group_name.label("GroupName"), + literal("Egress-VLAN-Name").label("Attribute"), + literal(":=").label("Op"), + literal("2hades-unauth").label("Value"), + ), # Fall-Through := No, blocking groups - Query([ - radius_property.c.property.label('GroupName'), - literal("Fall-Through").label('Attribute'), - literal(":=").label('Op'), - literal("No").label('Value'), - ]), + select( + radius_property.c.hades_group_name.label("GroupName"), + literal("Fall-Through").label("Attribute"), + literal(":=").label("Op"), + literal("No").label("Value"), + ), # Generic error group `no_network_access` # Same semantics as a specific error group Query([ From 0f042357f8d7d4ce598a19779dba35ee0b731966 Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 25 Oct 2023 20:24:59 +0200 Subject: [PATCH 2/9] Adapt `radgroupreply` test to correct fixtures --- tests/model/test_hades.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/model/test_hades.py b/tests/model/test_hades.py index 18b84d70f..684ede3e9 100644 --- a/tests/model/test_hades.py +++ b/tests/model/test_hades.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta import pytest +from sqlalchemy import select from pycroft.helpers.interval import closedopen from pycroft.model import hades @@ -69,10 +70,15 @@ def membership(module_session, user, network_access_group, now) -> Membership: @pytest.fixture(scope='module', autouse=True) def mapped_radius_properties(module_session) -> None: - module_session.execute(hades.radius_property.insert().values([ - ('payment_in_default',), - ('traffic_limit_exceeded',), - ])) + module_session.execute( + hades.radius_property.insert().values( + [ + ("payment_in_default", "pid", True), + ("traffic_limit_exceeded", "traffic", True), + ("non_blocking_group", "non_blocking", False), + ] + ) + ) class TestHadesView: @@ -123,11 +129,16 @@ def test_radgroupreply_access_groups(self, session): assert (group_name, "Fall-Through", ":=", "Yes") in rows def test_radgroupreply_blocking_groups(self, session): - props = [x[0] for x in session.query(hades.radius_property).all()] - rows = session.query(hades.radgroupreply.table).all() - for prop in props: - assert (prop, "Egress-VLAN-Name", ":=", "2hades-unauth") in rows - assert (prop, "Fall-Through", ":=", "No") in rows + rp = hades.radius_property + groups = session.execute(select(rp.c.hades_group_name, rp.c.is_blocking_group)) + rows = session.execute(select(hades.radgroupreply.table)).all() + for prop, is_blocking in groups: + if is_blocking: + assert (prop, "Egress-VLAN-Name", ":=", "2hades-unauth") in rows + assert (prop, "Fall-Through", ":=", "No") in rows + else: + assert (prop, "Egress-VLAN-Name", ":=", "2hades-unauth") not in rows + assert (prop, "Fall-Through", ":=", "No") not in rows def test_radusergroup_access(self, session, user): host = user.hosts[0] From 3c91610e6f0efb8c5cb3d7e038574b32c8532531 Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 25 Oct 2023 20:25:25 +0200 Subject: [PATCH 3/9] Only include blocking groups in certain `radgroupreply` settings Refs #504 --- pycroft/model/hades.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycroft/model/hades.py b/pycroft/model/hades.py index 373120b7c..7eff44bb2 100644 --- a/pycroft/model/hades.py +++ b/pycroft/model/hades.py @@ -231,14 +231,14 @@ literal("Egress-VLAN-Name").label("Attribute"), literal(":=").label("Op"), literal("2hades-unauth").label("Value"), - ), + ).where(radius_property.c.is_blocking_group), # Fall-Through := No, blocking groups select( radius_property.c.hades_group_name.label("GroupName"), literal("Fall-Through").label("Attribute"), literal(":=").label("Op"), literal("No").label("Value"), - ), + ).where(radius_property.c.is_blocking_group), # Generic error group `no_network_access` # Same semantics as a specific error group Query([ From b1dc6b3dcf64ca85dfa7995da3e0c071991865c1 Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 25 Oct 2023 20:44:06 +0200 Subject: [PATCH 4/9] Fix radusergroup test to test for mapped group name --- tests/model/test_hades.py | 45 ++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/tests/model/test_hades.py b/tests/model/test_hades.py index 684ede3e9..eaf2a2bc1 100644 --- a/tests/model/test_hades.py +++ b/tests/model/test_hades.py @@ -161,16 +161,25 @@ def test_dhcphost_access(self, session, user): assert row == (host.interfaces[0].mac, str(host.ips[0].address), host.name) -class TestHadesBlockedView: +class TestFinanceBlocking: + @pytest.fixture(scope="class") + def bad_group(self, payment_in_default_group): + return payment_in_default_group + + @pytest.fixture(scope="class") + def radius_group_name(self) -> str: + return "pid" + @pytest.fixture(scope='class', autouse=True) - def payment_in_default_membership(self, now, class_session, user, payment_in_default_group): + def bad_membership(self, now, class_session, user, bad_group): return MembershipFactory.create( - user=user, group=payment_in_default_group, + user=user, + group=bad_group, begins_at=now + timedelta(-1), ends_at=now + timedelta(1) ) - def test_radusergroup_blocked(self, session, user): + def test_radusergroup_blocked(self, session, user, radius_group_name): host = user.hosts[0] switch_ports = [p.switch_port for p in host.room.connected_patch_ports] assert len(host.ips) == 1 @@ -179,11 +188,31 @@ def test_radusergroup_blocked(self, session, user): rows = session.query(hades.radusergroup.table).all() for switch_port in switch_ports: - assert (mac, str(switch_port.switch.management_ip), switch_port.name, - 'payment_in_default', -10) in rows - assert (mac, str(switch_port.switch.management_ip), switch_port.name, - 'no_network_access', 0) in rows + assert ( + mac, + str(switch_port.switch.management_ip), + switch_port.name, + radius_group_name, + -10, + ) in rows + assert ( + mac, + str(switch_port.switch.management_ip), + switch_port.name, + "no_network_access", + 0, + ) in rows def test_dhcphost_blocked(self, session): rows = session.query(hades.dhcphost.table).all() assert len(rows) == 0 + + +class TestTrafficBlocking(TestFinanceBlocking): + @pytest.fixture(scope="class") + def bad_group(self, traffic_limit_exceeded_group): + return traffic_limit_exceeded_group + + @pytest.fixture(scope="class") + def radius_group_name(self) -> str: + return "traffic" From f0209c983d33e23fb87cce15308457c1d7cf252f Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 25 Oct 2023 21:02:49 +0200 Subject: [PATCH 5/9] Use mapped group name in radusergroup view definition This fixes the feature test implemented earlier. Refs #504 --- pycroft/model/hades.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pycroft/model/hades.py b/pycroft/model/hades.py index 7eff44bb2..9fdc4f04d 100644 --- a/pycroft/model/hades.py +++ b/pycroft/model/hades.py @@ -93,13 +93,16 @@ # Priority -10: Blocking reason exists # @ / (Prio -10) # Note that Fall-Through:=No for blocking groups, so first match terminates - Query([ - Interface.mac.label('UserName'), - func.host(Switch.management_ip).label('NASIPAddress'), - SwitchPort.name.label('NASPortId'), - radius_property.c.property.label('GroupName'), - literal(-10).label('Priority'), - ]).select_from(User) + # Also, priority 10: some other custom radius group + # @ / (Prio -10) + select( + Interface.mac.label("UserName"), + func.host(Switch.management_ip).label("NASIPAddress"), + SwitchPort.name.label("NASPortId"), + radius_property.c.hades_group_name.label("GroupName"), + literal(-10).label("Priority"), + ) + .select_from(User) .join(Host) .join(Host.interfaces) .join(Host.room) @@ -107,10 +110,9 @@ .join(SwitchPort) .join(Switch) .join(User.current_properties) - .join(radius_property, - radius_property.c.property == CurrentProperty.property_name) - .statement, - + .join( + radius_property, radius_property.c.property == CurrentProperty.property_name + ), # Priority 0: No blocking reason exists → generic error group `no_network_access` Query([ Interface.mac.label('UserName'), From 50b0ee9f414a67d744f5df3b690b19f691b58d87 Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 25 Oct 2023 21:25:46 +0200 Subject: [PATCH 6/9] Add radusergroup test for non-blocking group --- tests/model/test_hades.py | 61 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/tests/model/test_hades.py b/tests/model/test_hades.py index eaf2a2bc1..2fa5e8370 100644 --- a/tests/model/test_hades.py +++ b/tests/model/test_hades.py @@ -5,7 +5,7 @@ from pycroft.helpers.interval import closedopen from pycroft.model import hades -from pycroft.model.host import Switch +from pycroft.model.host import Switch, Host from pycroft.model.net import VLAN from pycroft.model.user import PropertyGroup, User, Membership from tests.factories import PropertyGroupFactory, MembershipFactory, \ @@ -38,6 +38,14 @@ def traffic_limit_exceeded_group(module_session) -> PropertyGroup: ) +@pytest.fixture(scope="module", autouse=True) +def non_blocking_group(module_session) -> PropertyGroup: + return PropertyGroupFactory.create( + name="Non-blocking", + granted={"non_blocking_group"}, + ) + + @pytest.fixture(scope='module', autouse=True) def user(module_session) -> User: return UserFactory(with_host=True) @@ -161,6 +169,13 @@ def test_dhcphost_access(self, session, user): assert row == (host.interfaces[0].mac, str(host.ips[0].address), host.name) +def mac_from_host(host: Host): + assert len(host.ips) == 1 + assert len(host.interfaces) == 1 + mac = host.interfaces[0].mac + return mac + + class TestFinanceBlocking: @pytest.fixture(scope="class") def bad_group(self, payment_in_default_group): @@ -182,9 +197,7 @@ def bad_membership(self, now, class_session, user, bad_group): def test_radusergroup_blocked(self, session, user, radius_group_name): host = user.hosts[0] switch_ports = [p.switch_port for p in host.room.connected_patch_ports] - assert len(host.ips) == 1 - assert len(host.interfaces) == 1 - mac = host.interfaces[0].mac + mac = mac_from_host(host) rows = session.query(hades.radusergroup.table).all() for switch_port in switch_ports: @@ -216,3 +229,43 @@ def bad_group(self, traffic_limit_exceeded_group): @pytest.fixture(scope="class") def radius_group_name(self) -> str: return "traffic" + + +class TestNonBlockingGroup: + @pytest.fixture(scope="class", autouse=True) + def non_blocking_membership(self, class_session, user, non_blocking_group, now): + return MembershipFactory.create( + user=user, + group=non_blocking_group, + begins_at=now + timedelta(-1), + ends_at=None, + ) + + def test_radusergroup_non_blocking(self, session, user): + radius_group_name = "non_blocking" + host = user.hosts[0] + mac = mac_from_host(host) + switch_ports = [p.switch_port for p in host.room.connected_patch_ports] + rows = session.execute(select(hades.radusergroup.table)).all() + for switch_port in switch_ports: + mgmt_ip = str(switch_port.switch.management_ip) + assert ( + mac, + mgmt_ip, + switch_port.name, + radius_group_name, + 10, + ) in rows, ( + "radusergroup does not contain row " + f"for non-blocking custom group {radius_group_name!r}" + ) + assert ( + mac, + mgmt_ip, + switch_port.name, + "no_network_access", + 0, + ) not in rows, ( + "radusergroup contains a `no_network_access` row " + "for user with non-blocking custom group" + ) From 16d2376d0d07d70c0bed4f03eb52590e769e4242 Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 25 Oct 2023 21:27:54 +0200 Subject: [PATCH 7/9] Set radusergroup priority reasonably for non-blocking groups Refs #504 --- pycroft/model/hades.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pycroft/model/hades.py b/pycroft/model/hades.py index 9fdc4f04d..570d7fcec 100644 --- a/pycroft/model/hades.py +++ b/pycroft/model/hades.py @@ -15,6 +15,7 @@ and_, Boolean, select, + case, ) from sqlalchemy.orm import Query, aliased, configure_mappers @@ -100,7 +101,10 @@ func.host(Switch.management_ip).label("NASIPAddress"), SwitchPort.name.label("NASPortId"), radius_property.c.hades_group_name.label("GroupName"), - literal(-10).label("Priority"), + case( + (radius_property.c.is_blocking_group, literal(-10)), + else_=literal(10), + ).label("Priority"), ) .select_from(User) .join(Host) From 1d79d79bdd29bd6ec0d243ace2e4745d38d352be Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Thu, 26 Oct 2023 11:34:38 +0200 Subject: [PATCH 8/9] Commit transaction when calling `drop-model` `connect()` without a `begin()` does not leave the contect manager with a `commit`. Instead of calling `connect()` and then `begin()` or calling `commit()` manually, we can use `Engine.begin()` directly[1]. [1] https://docs.sqlalchemy.org/en/20/core/connections.html#connect-and-begin-once-from-the-engine --- web/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/commands.py b/web/commands.py index 96bda96bb..4959993ca 100644 --- a/web/commands.py +++ b/web/commands.py @@ -27,5 +27,5 @@ def drop_model() -> None: engine = create_engine(os.getenv('PYCROFT_DB_URI')) click.confirm(f'This will drop the whole database schema associated to {engine!r}.' ' Are you absolutely sure?', abort=True) - with engine.connect() as connection: + with engine.begin() as connection: drop_db_model(bind=connection) From 0cf20550ba13ea2768c981dde2240e1527caea4b Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Thu, 26 Oct 2023 11:41:59 +0200 Subject: [PATCH 9/9] Add migrations for radius_property changes These need to be extra careful in their idempotence as some of these changes (notably, the `radius_property.hades_group_name` column) have already been made in production. Refs #504 --- pycroft/model/alembic.ini | 2 +- ...4_add_custom_non_blocking_radius_groups.py | 285 ++++++++++++++++++ 2 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 pycroft/model/alembic/versions/55e9f0d9b5f4_add_custom_non_blocking_radius_groups.py diff --git a/pycroft/model/alembic.ini b/pycroft/model/alembic.ini index 7587a7a9d..a249f6052 100644 --- a/pycroft/model/alembic.ini +++ b/pycroft/model/alembic.ini @@ -60,7 +60,7 @@ handlers = qualname = sqlalchemy.engine [logger_alembic] -level = INFO +level = DEBUG handlers = qualname = alembic diff --git a/pycroft/model/alembic/versions/55e9f0d9b5f4_add_custom_non_blocking_radius_groups.py b/pycroft/model/alembic/versions/55e9f0d9b5f4_add_custom_non_blocking_radius_groups.py new file mode 100644 index 000000000..64404102b --- /dev/null +++ b/pycroft/model/alembic/versions/55e9f0d9b5f4_add_custom_non_blocking_radius_groups.py @@ -0,0 +1,285 @@ +"""Add custom non-blocking radius groups + +Revision ID: 55e9f0d9b5f4 +Revises: 249743eb7b94 +Create Date: 2023-10-26 08:34:11.939772 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "55e9f0d9b5f4" +down_revision = "249743eb7b94" +branch_labels = None +depends_on = None + + +def upgrade(): + # unfortunately, `op.add_column` does not support `if not exists` + op.execute( + "ALTER TABLE radius_property ADD COLUMN IF NOT EXISTS hades_group_name varchar" + ) + op.add_column("radius_property", sa.Column("is_blocking_group", sa.Boolean())) + + ## DATA MIGRATION + NAME_MAPPING = ( + ("payment_in_default", "default_in_payment"), + ("traffic_limit_exceeded", "traffic"), + ("ZW41_acltestproperty", "ZW41_untagged_acltest"), + ("Wu3_acltestproperty", "Wu3_untagged_acltest"), + ) + for p, hgn in NAME_MAPPING: + op.execute( + f"update radius_property set hades_group_name='{hgn}' where property='{p}'" + ) + op.execute( + "update radius_property set hades_group_name=property where hades_group_name is null" + ) + + BLOCKING_PROPS = ("violation", "payment_in_default", "traffic_limit_exceeded") + blocking_props_list = ", ".join(f"'{p}'" for p in BLOCKING_PROPS) + op.execute( + f"update radius_property set is_blocking_group=(property in ({blocking_props_list}));" + ) + + op.alter_column("radius_property", "hades_group_name", nullable=False) + op.alter_column("radius_property", "is_blocking_group", nullable=False) + ## /DATA MIGRATION + + op.execute( + """ + CREATE OR REPLACE VIEW radusergroup AS + SELECT interface.mac AS "UserName", + host(switch.management_ip) AS "NASIPAddress", + switch_port.name AS "NASPortId", + vlan.name::text || '_untagged'::text AS "GroupName", + 20 AS "Priority" + FROM "user" + JOIN host ON "user".id = host.owner_id + JOIN interface ON host.id = interface.host_id + JOIN room ON room.id = host.room_id + JOIN patch_port ON patch_port.room_id = room.id AND patch_port.switch_port_id IS NOT NULL + JOIN switch_port ON switch_port.id = patch_port.switch_port_id + JOIN switch ON switch.host_id = switch_port.switch_id + JOIN ip ON interface.id = ip.interface_id + JOIN subnet ON subnet.id = ip.subnet_id + JOIN vlan ON vlan.id = subnet.vlan_id + JOIN current_property ON "user".id = current_property.user_id AND NOT current_property.denied + WHERE current_property.property_name::text = 'network_access'::text + UNION ALL + SELECT interface.mac AS "UserName", + host(switch.management_ip) AS "NASIPAddress", + switch_port.name AS "NASPortId", + radius_property.hades_group_name AS "GroupName", + CASE + WHEN radius_property.is_blocking_group THEN '-10'::integer + ELSE 10 + END AS "Priority" + FROM "user" + JOIN host ON "user".id = host.owner_id + JOIN interface ON host.id = interface.host_id + JOIN room ON room.id = host.room_id + JOIN patch_port ON patch_port.room_id = room.id AND patch_port.switch_port_id IS NOT NULL + JOIN switch_port ON switch_port.id = patch_port.switch_port_id + JOIN switch ON switch.host_id = switch_port.switch_id + JOIN current_property ON "user".id = current_property.user_id AND NOT current_property.denied + JOIN radius_property ON radius_property.property::text = current_property.property_name::text + UNION ALL + SELECT interface.mac AS "UserName", + host(switch.management_ip) AS "NASIPAddress", + switch_port.name AS "NASPortId", + 'no_network_access'::text AS "GroupName", + 0 AS "Priority" + FROM "user" + LEFT JOIN ( SELECT current_property.user_id, + 1 AS network_access + FROM current_property + WHERE current_property.property_name::text = 'network_access'::text AND NOT current_property.denied) users_with_network_access + ON "user".id = users_with_network_access.user_id + JOIN host ON "user".id = host.owner_id + JOIN interface ON host.id = interface.host_id + JOIN room ON room.id = host.room_id + JOIN patch_port ON patch_port.room_id = room.id AND patch_port.switch_port_id IS NOT NULL + JOIN switch_port ON switch_port.id = patch_port.switch_port_id + JOIN switch ON switch.host_id = switch_port.switch_id + WHERE users_with_network_access.network_access IS NULL; + """ + ) + op.execute( + """ + CREATE OR REPLACE VIEW radgroupreply AS + SELECT radgroupreply_base."GroupName", + radgroupreply_base."Attribute", + radgroupreply_base."Op", + radgroupreply_base."Value" + FROM radgroupreply_base + UNION ALL + SELECT vlan.name::text || '_untagged'::text AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + '+='::character varying AS "Op", + '2'::text || vlan.name::text AS "Value" + FROM vlan + UNION ALL + SELECT vlan.name::text || '_tagged'::text AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + '+='::character varying AS "Op", + '1'::text || vlan.name::text AS "Value" + FROM vlan + UNION ALL + SELECT vlan.name::text || '_untagged'::text AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'Yes'::character varying AS "Value" + FROM vlan + UNION ALL + SELECT vlan.name::text || '_tagged'::text AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'Yes'::character varying AS "Value" + FROM vlan + UNION ALL + SELECT radius_property.hades_group_name AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + ':='::character varying AS "Op", + '2hades-unauth'::character varying AS "Value" + FROM radius_property + WHERE radius_property.is_blocking_group + UNION ALL + SELECT radius_property.hades_group_name AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'No'::character varying AS "Value" + FROM radius_property + WHERE radius_property.is_blocking_group + UNION ALL + SELECT 'no_network_access'::character varying AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + ':='::character varying AS "Op", + '2hades-unauth'::character varying AS "Value" + UNION ALL + SELECT 'no_network_access'::character varying AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'No'::character varying AS "Value"; + """ + ) + + +def downgrade(): + op.execute( + """ + CREATE OR REPLACE VIEW radgroupreply AS + SELECT radgroupreply_base."GroupName", + radgroupreply_base."Attribute", + radgroupreply_base."Op", + radgroupreply_base."Value" + FROM radgroupreply_base + UNION ALL + SELECT vlan.name::text || '_untagged'::text AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + '+='::character varying AS "Op", + '2'::text || vlan.name::text AS "Value" + FROM vlan + UNION ALL + SELECT vlan.name::text || '_tagged'::text AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + '+='::character varying AS "Op", + '1'::text || vlan.name::text AS "Value" + FROM vlan + UNION ALL + SELECT vlan.name::text || '_untagged'::text AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'Yes'::character varying AS "Value" + FROM vlan + UNION ALL + SELECT vlan.name::text || '_tagged'::text AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'Yes'::character varying AS "Value" + FROM vlan + UNION ALL + SELECT radius_property.property AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + ':='::character varying AS "Op", + '2hades-unauth'::character varying AS "Value" + FROM radius_property + UNION ALL + SELECT radius_property.property AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'No'::character varying AS "Value" + FROM radius_property + UNION ALL + SELECT 'no_network_access'::character varying AS "GroupName", + 'Egress-VLAN-Name'::character varying AS "Attribute", + ':='::character varying AS "Op", + '2hades-unauth'::character varying AS "Value" + UNION ALL + SELECT 'no_network_access'::character varying AS "GroupName", + 'Fall-Through'::character varying AS "Attribute", + ':='::character varying AS "Op", + 'No'::character varying AS "Value"; + """ + ) + op.execute( + """ + CREATE OR REPLACE VIEW radusergroup AS + SELECT interface.mac AS "UserName", + host(switch.management_ip) AS "NASIPAddress", + switch_port.name AS "NASPortId", + vlan.name::text || '_untagged'::text AS "GroupName", + 20 AS "Priority" + FROM "user" + JOIN host ON "user".id = host.owner_id + JOIN interface ON host.id = interface.host_id + JOIN room ON room.id = host.room_id + JOIN patch_port ON patch_port.room_id = room.id AND patch_port.switch_port_id IS NOT NULL + JOIN switch_port ON switch_port.id = patch_port.switch_port_id + JOIN switch ON switch.host_id = switch_port.switch_id + JOIN ip ON interface.id = ip.interface_id + JOIN subnet ON subnet.id = ip.subnet_id + JOIN vlan ON vlan.id = subnet.vlan_id + JOIN current_property ON "user".id = current_property.user_id AND NOT current_property.denied + WHERE current_property.property_name::text = 'network_access'::text + UNION ALL + SELECT interface.mac AS "UserName", + host(switch.management_ip) AS "NASIPAddress", + switch_port.name AS "NASPortId", + radius_property.property AS "GroupName", + '-10'::integer AS "Priority" + FROM "user" + JOIN host ON "user".id = host.owner_id + JOIN interface ON host.id = interface.host_id + JOIN room ON room.id = host.room_id + JOIN patch_port ON patch_port.room_id = room.id AND patch_port.switch_port_id IS NOT NULL + JOIN switch_port ON switch_port.id = patch_port.switch_port_id + JOIN switch ON switch.host_id = switch_port.switch_id + JOIN current_property ON "user".id = current_property.user_id AND NOT current_property.denied + JOIN radius_property ON radius_property.property::text = current_property.property_name::text + UNION ALL + SELECT interface.mac AS "UserName", + host(switch.management_ip) AS "NASIPAddress", + switch_port.name AS "NASPortId", + 'no_network_access'::text AS "GroupName", + 0 AS "Priority" + FROM "user" + LEFT JOIN ( SELECT current_property.user_id, + 1 AS network_access + FROM current_property + WHERE current_property.property_name::text = 'network_access'::text AND NOT current_property.denied) users_with_network_access + ON "user".id = users_with_network_access.user_id + JOIN host ON "user".id = host.owner_id + JOIN interface ON host.id = interface.host_id + JOIN room ON room.id = host.room_id + JOIN patch_port ON patch_port.room_id = room.id AND patch_port.switch_port_id IS NOT NULL + JOIN switch_port ON switch_port.id = patch_port.switch_port_id + JOIN switch ON switch.host_id = switch_port.switch_id + WHERE users_with_network_access.network_access IS NULL; + """ + ) + + op.drop_column("radius_property", "is_blocking_group") + op.drop_column("radius_property", "hades_group_name")