feat: allow admins to bypass approval flow when joining networks
This commit is contained in:
@@ -13,12 +13,12 @@ from gatehouse_app.services import device_service
|
||||
from gatehouse_app.services import network_access_service
|
||||
from gatehouse_app.services import zerotier_api_service as zt
|
||||
from gatehouse_app.services import zerotier_reconciliation_service
|
||||
from gatehouse_app.services.user_service import UserService
|
||||
from gatehouse_app.models import (
|
||||
PortalNetwork,
|
||||
Device,
|
||||
DeviceNetworkMembership,
|
||||
UserNetworkApproval,
|
||||
ActivationSession,
|
||||
NetworkAccessRequest,
|
||||
)
|
||||
from gatehouse_app.models.organization import Organization
|
||||
from gatehouse_app.models.organization.organization_member import OrganizationMember
|
||||
@@ -30,7 +30,6 @@ from gatehouse_app.exceptions import (
|
||||
DeviceNotFoundError,
|
||||
DeviceAlreadyExistsError,
|
||||
ApprovalNotFoundError,
|
||||
MembershipNotFoundError,
|
||||
)
|
||||
|
||||
|
||||
@@ -347,6 +346,47 @@ def list_devices(org_id):
|
||||
)
|
||||
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>/users/<user_id>/devices", methods=["GET"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def list_user_devices(org_id, user_id):
|
||||
"""List all ZeroTier devices for a specific user in the organization (admin only)."""
|
||||
org, err = _org_check(org_id)
|
||||
if err:
|
||||
return err
|
||||
|
||||
# Verify target user exists
|
||||
from gatehouse_app.exceptions.validation_exceptions import UserNotFoundError
|
||||
try:
|
||||
target_user = UserService.get_user_by_id(user_id)
|
||||
except UserNotFoundError:
|
||||
return api_response(success=False, message="User not found", status=404, error_type="NOT_FOUND")
|
||||
|
||||
# Verify target user is a member of the org
|
||||
is_member = OrganizationMember.query.filter(
|
||||
OrganizationMember.organization_id == org_id,
|
||||
OrganizationMember.user_id == user_id,
|
||||
OrganizationMember.deleted_at.is_(None),
|
||||
).first() is not None
|
||||
|
||||
if not is_member:
|
||||
return api_response(success=False, message="User is not a member of this organization", status=404, error_type="NOT_FOUND")
|
||||
|
||||
# Get devices for the user in this org
|
||||
devices = device_service.list_user_devices(user_id, org_id)
|
||||
|
||||
return api_response(
|
||||
data={
|
||||
"devices": [d.to_dict() for d in devices],
|
||||
"count": len(devices),
|
||||
"user_id": user_id,
|
||||
"organization_id": org_id,
|
||||
},
|
||||
message="User devices retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>/devices", methods=["POST"])
|
||||
@login_required
|
||||
@full_access_required
|
||||
@@ -373,11 +413,8 @@ def register_device(org_id):
|
||||
serial_number=data.get("serial_number"),
|
||||
)
|
||||
|
||||
from gatehouse_app.services.network_access_service import materialize_device_memberships
|
||||
memberships = materialize_device_memberships(device.id)
|
||||
|
||||
return api_response(
|
||||
data={"device": device.to_dict(), "memberships_created": len(memberships)},
|
||||
data={"device": device.to_dict()},
|
||||
message="Device registered successfully",
|
||||
status=201,
|
||||
)
|
||||
@@ -486,7 +523,7 @@ def list_my_approvals(org_id):
|
||||
if err:
|
||||
return err
|
||||
|
||||
approvals = network_access_service.list_user_approvals(g.current_user.id, org_id)
|
||||
approvals = network_access_service.list_user_requests(g.current_user.id, org_id)
|
||||
return api_response(
|
||||
data={"approvals": [a.to_dict() for a in approvals], "count": len(approvals)},
|
||||
message="Approvals retrieved successfully",
|
||||
@@ -549,18 +586,18 @@ def reject_request(org_id, approval_id):
|
||||
return api_response(success=False, message=str(e.message), status=400, error_type=e.error_type)
|
||||
|
||||
|
||||
@api_v1_bp.route("/organizations/<org_id>/approvals/<approval_id>/revoke", methods=["POST"])
|
||||
@api_v1_bp.route("/organizations/<org_id>/approvals/<request_id>/revoke", methods=["POST"])
|
||||
@login_required
|
||||
@require_admin
|
||||
@full_access_required
|
||||
def revoke_approval(org_id, approval_id):
|
||||
def revoke_approval(org_id, request_id):
|
||||
"""Revoke an approved access record (admin only)."""
|
||||
org, err = _org_check(org_id)
|
||||
if err:
|
||||
return err
|
||||
|
||||
try:
|
||||
approval = network_access_service.revoke_approval(approval_id, g.current_user.id)
|
||||
approval = network_access_service.revoke_access(request_id, g.current_user.id)
|
||||
return api_response(data={"approval": approval.to_dict()}, message="Approval revoked successfully")
|
||||
except ApprovalNotFoundError as e:
|
||||
return api_response(success=False, message=str(e), status=404, error_type=e.error_type)
|
||||
@@ -607,7 +644,7 @@ def admin_list_all_approvals(org_id):
|
||||
|
||||
network_id = request.args.get("network_id")
|
||||
state = request.args.get("state")
|
||||
approvals = network_access_service.list_all_org_approvals(org_id, network_id=network_id, state=state)
|
||||
approvals = network_access_service.list_all_org_requests(org_id, network_id=network_id, state=state)
|
||||
return api_response(
|
||||
data={"approvals": [a.to_dict() for a in approvals], "count": len(approvals)},
|
||||
message="Approvals retrieved successfully",
|
||||
@@ -626,10 +663,10 @@ def list_memberships(org_id):
|
||||
if err:
|
||||
return err
|
||||
|
||||
memberships = DeviceNetworkMembership.query.filter(
|
||||
DeviceNetworkMembership.user_id == g.current_user.id,
|
||||
DeviceNetworkMembership.organization_id == org_id,
|
||||
DeviceNetworkMembership.deleted_at.is_(None),
|
||||
memberships = NetworkAccessRequest.query.filter(
|
||||
NetworkAccessRequest.user_id == g.current_user.id,
|
||||
NetworkAccessRequest.organization_id == org_id,
|
||||
NetworkAccessRequest.deleted_at.is_(None),
|
||||
).all()
|
||||
|
||||
return api_response(
|
||||
@@ -656,15 +693,14 @@ def activate_membership(org_id, membership_id):
|
||||
is_admin = _is_org_admin(org_id, g.current_user.id)
|
||||
|
||||
try:
|
||||
session = network_access_service.activate_device_membership(
|
||||
membership_id=membership_id,
|
||||
session = network_access_service.activate_request(
|
||||
request_id=membership_id,
|
||||
user_id=g.current_user.id,
|
||||
lifetime_minutes=data.get("lifetime_minutes"),
|
||||
admin_override=is_admin,
|
||||
)
|
||||
membership = DeviceNetworkMembership.query.get(membership_id)
|
||||
return api_response(data={"session": session.to_dict(), "membership": membership.to_dict()}, message="Membership activated successfully")
|
||||
except MembershipNotFoundError as e:
|
||||
return api_response(data={"session": session.to_dict()}, message="Request activated successfully")
|
||||
except ApprovalNotFoundError as e:
|
||||
return api_response(success=False, message=str(e), status=404, error_type=e.error_type)
|
||||
except AppValidationError as e:
|
||||
return api_response(success=False, message=str(e.message), status=400, error_type=e.error_type)
|
||||
@@ -681,22 +717,22 @@ def deactivate_membership(org_id, membership_id):
|
||||
|
||||
# Verify ownership for non-admins
|
||||
if not _is_org_admin(org_id, g.current_user.id):
|
||||
membership_check = DeviceNetworkMembership.query.filter(
|
||||
DeviceNetworkMembership.id == membership_id,
|
||||
DeviceNetworkMembership.user_id == g.current_user.id,
|
||||
DeviceNetworkMembership.deleted_at.is_(None),
|
||||
membership_check = NetworkAccessRequest.query.filter(
|
||||
NetworkAccessRequest.id == membership_id,
|
||||
NetworkAccessRequest.user_id == g.current_user.id,
|
||||
NetworkAccessRequest.deleted_at.is_(None),
|
||||
).first()
|
||||
if not membership_check:
|
||||
return api_response(success=False, message="Membership not found", status=404, error_type="NOT_FOUND")
|
||||
|
||||
try:
|
||||
membership = network_access_service.deactivate_membership(
|
||||
membership_id=membership_id,
|
||||
req = network_access_service.deactivate_request(
|
||||
request_id=membership_id,
|
||||
reason="manual_revoke",
|
||||
deactivated_by_user_id=g.current_user.id,
|
||||
)
|
||||
return api_response(data={"membership": membership.to_dict()}, message="Membership deactivated successfully")
|
||||
except MembershipNotFoundError as e:
|
||||
return api_response(data={"request": req.to_dict()}, message="Request deactivated successfully")
|
||||
except ApprovalNotFoundError as e:
|
||||
return api_response(success=False, message=str(e), status=404, error_type=e.error_type)
|
||||
|
||||
|
||||
@@ -730,17 +766,21 @@ def activate_all_memberships(org_id):
|
||||
@login_required
|
||||
@full_access_required
|
||||
def join_network(org_id, device_id, portal_network_id):
|
||||
"""Join an open network directly with a registered device."""
|
||||
"""Join an open network directly with a registered device. Admins can override for any network."""
|
||||
org, err = _org_check(org_id)
|
||||
if err:
|
||||
return err
|
||||
|
||||
is_admin = _is_org_admin(org_id, g.current_user.id)
|
||||
|
||||
try:
|
||||
membership = network_access_service.join_network_for_device(
|
||||
user_id=g.current_user.id,
|
||||
organization_id=org_id,
|
||||
device_id=device_id,
|
||||
portal_network_id=portal_network_id,
|
||||
admin_override=is_admin,
|
||||
granted_by_user_id=g.current_user.id if is_admin else None,
|
||||
)
|
||||
return api_response(data={"membership": membership.to_dict()}, message="Joined network successfully", status=201)
|
||||
except AppValidationError as e:
|
||||
@@ -759,12 +799,12 @@ def delete_membership(org_id, membership_id):
|
||||
return err
|
||||
|
||||
try:
|
||||
network_access_service.revoke_membership_soft(
|
||||
membership_id=membership_id,
|
||||
revoked_by_user_id=g.current_user.id,
|
||||
network_access_service.revoke_request_soft(
|
||||
request_id=membership_id,
|
||||
revoker_user_id=g.current_user.id,
|
||||
)
|
||||
return api_response(message="Membership removed successfully")
|
||||
except MembershipNotFoundError as e:
|
||||
return api_response(message="Request revoked successfully")
|
||||
except ApprovalNotFoundError as e:
|
||||
return api_response(success=False, message=str(e), status=404, error_type=e.error_type)
|
||||
|
||||
|
||||
@@ -820,10 +860,8 @@ def end_session(org_id, session_id):
|
||||
|
||||
_end_session(session, ActivationEndReason.LOGOUT)
|
||||
|
||||
membership = DeviceNetworkMembership.query.get(session.device_network_membership_id)
|
||||
if membership:
|
||||
from gatehouse_app.services.network_access_service import deactivate_membership
|
||||
deactivate_membership(membership.id, reason="logout")
|
||||
if session.network_access_request_id:
|
||||
network_access_service.deactivate_request(session.network_access_request_id, reason="logout")
|
||||
|
||||
return api_response(message="Session ended successfully")
|
||||
|
||||
@@ -848,15 +886,16 @@ def trigger_kill_switch(org_id):
|
||||
return api_response(success=False, message="Validation failed", status=400, error_type="VALIDATION_ERROR", error_details=e.messages)
|
||||
|
||||
try:
|
||||
event = network_access_service.kill_switch(
|
||||
target_user_id=data["target_user_id"],
|
||||
triggered_by_user_id=g.current_user.id,
|
||||
organization_id=org_id,
|
||||
scope=data.get("scope", "organization"),
|
||||
reason=data.get("reason"),
|
||||
from gatehouse_app.utils.constants import KillSwitchScope
|
||||
scope = data.get("scope", "organization")
|
||||
scope_enum = KillSwitchScope(scope) if scope in KillSwitchScope._value2member_map_ else KillSwitchScope.ORGANIZATION
|
||||
count = network_access_service.kill_switch(
|
||||
user_id=data["target_user_id"],
|
||||
org_id=org_id,
|
||||
scope=scope_enum,
|
||||
network_ids=data.get("network_ids"),
|
||||
)
|
||||
return api_response(data={"event": event.to_dict()}, message="Kill switch triggered successfully")
|
||||
return api_response(data={"affected_count": count}, message="Kill switch triggered successfully")
|
||||
except AppValidationError as e:
|
||||
return api_response(success=False, message=str(e.message), status=400, error_type=e.error_type)
|
||||
|
||||
@@ -873,10 +912,10 @@ def admin_list_memberships(org_id):
|
||||
if err:
|
||||
return err
|
||||
|
||||
memberships = network_access_service.get_all_memberships_with_details(org_id)
|
||||
requests = network_access_service.get_all_requests_with_details(org_id)
|
||||
return api_response(
|
||||
data={"memberships": memberships, "count": len(memberships)},
|
||||
message="All memberships retrieved successfully",
|
||||
data={"requests": requests, "count": len(requests)},
|
||||
message="All requests retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@@ -891,9 +930,9 @@ def admin_delete_membership(org_id, membership_id):
|
||||
return err
|
||||
|
||||
try:
|
||||
network_access_service.hard_delete_membership(membership_id)
|
||||
return api_response(message="Membership permanently deleted")
|
||||
except MembershipNotFoundError as e:
|
||||
network_access_service.hard_delete_request(membership_id)
|
||||
return api_response(message="Request permanently deleted")
|
||||
except ApprovalNotFoundError as e:
|
||||
return api_response(success=False, message=str(e), status=404, error_type=e.error_type)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user