Skip to content

Commit

Permalink
Implement a basic user session management (#104)
Browse files Browse the repository at this point in the history
This patch introduces a basic user session management that relies on the
/etc/machine-id and the effective user id
  • Loading branch information
r0x0d authored Jan 8, 2025
1 parent b496988 commit d8ccf9e
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 3 deletions.
71 changes: 71 additions & 0 deletions command_line_assistant/daemon/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Session management module for the daemon."""

import logging
import uuid
from pathlib import Path
from typing import Optional

#: Path to the machine ID file
MACHINE_ID_PATH: Path = Path("/etc/machine-id")

logger = logging.getLogger(__name__)


class UserSessionManager:
"""Manage user session information."""

def __init__(self, effective_user: str) -> None:
"""Initialize the session manager.
Args:
effective_user (str): The effective user id
"""
self._machine_uuid: Optional[uuid.UUID] = None
self._effective_user: str = effective_user
self._session_uuid: Optional[uuid.UUID] = None

@property
def machine_id(self) -> uuid.UUID:
"""Property that holds the machine UUID.
Reference:
https://www.freedesktop.org/software/systemd/man/latest/machine-id.html
Raises:
FileNotFoundError: If the machine-id file doesn't exist
ValueError: If the machine-id file is empty or malformed
Returns:
uuid.UUID: The UUID generated from machine-id
"""
if not self._machine_uuid:
try:
machine_id = MACHINE_ID_PATH.read_text().strip()
if not machine_id:
logger.error("Machine ID file is empty")
raise ValueError(f"Machine ID at {MACHINE_ID_PATH} is empty")
# Create a UUID from the machine-id string
self._machine_uuid = uuid.UUID(machine_id)
except FileNotFoundError as e:
logger.error("Machine ID file not found at %s", MACHINE_ID_PATH)
raise FileNotFoundError(
f"Machine ID file not found at {MACHINE_ID_PATH}"
) from e

return self._machine_uuid

@property
def session_id(self) -> uuid.UUID:
"""Property that generates a unique session ID combining machine and user effective id.
Returns:
uuid.UUID: A unique session identifier
"""
if not self._session_uuid:
# Combine machine ID and effective user to create a unique namespace
namespace = self.machine_id

# Generate a UUID using the effective username as name in the namespace
self._session_uuid = uuid.uuid5(namespace, self._effective_user)

return self._session_uuid
6 changes: 3 additions & 3 deletions docs/source/daemon/clad.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Command Line Assistant Daemon
=============================
Session
=======

.. automodule:: command_line_assistant.daemon.clad
.. automodule:: command_line_assistant.daemon.session
:members:
:undoc-members:
:private-members:
Expand Down
8 changes: 8 additions & 0 deletions docs/source/daemon/session.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Command Line Assistant Daemon
=============================

.. automodule:: command_line_assistant.daemon.clad
:members:
:undoc-members:
:private-members:
:no-index:
96 changes: 96 additions & 0 deletions tests/daemon/test_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import uuid
from unittest.mock import patch

import pytest

from command_line_assistant.daemon.session import UserSessionManager


def test_initialize_user_session_manager():
session = UserSessionManager("1000")
assert session._effective_user == "1000"
assert not session._machine_uuid
assert not session._session_uuid


def test_read_machine_id(tmp_path):
machine_id = tmp_path / "machine-id"
machine_id.write_text("09e28913cb074ed995a239c93b07fd8a")
with patch("command_line_assistant.daemon.session.MACHINE_ID_PATH", machine_id):
session = UserSessionManager("1000")
assert session.machine_id == uuid.UUID("09e28913cb074ed995a239c93b07fd8a")


def test_generate_session_id(tmp_path):
machine_id = tmp_path / "machine-id"
machine_id.write_text("09e28913cb074ed995a239c93b07fd8a")
with patch("command_line_assistant.daemon.session.MACHINE_ID_PATH", machine_id):
session = UserSessionManager("1000")
assert session.session_id == uuid.UUID("4d465f1c-0507-5dfa-9ea0-e2de1a9e90a5")


def test_generate_session_id_twice(tmp_path):
"""This verifies that the session ID is generated only once."""
machine_id = tmp_path / "machine-id"
machine_id.write_text("09e28913cb074ed995a239c93b07fd8a")
with patch("command_line_assistant.daemon.session.MACHINE_ID_PATH", machine_id):
session = UserSessionManager("1000")
assert session.session_id == uuid.UUID("4d465f1c-0507-5dfa-9ea0-e2de1a9e90a5")

session = UserSessionManager("1000")
assert session.session_id == uuid.UUID("4d465f1c-0507-5dfa-9ea0-e2de1a9e90a5")


@pytest.mark.parametrize(
("machine_id", "effective_user_id", "expected"),
(
(
"09e28913cb074ed995a239c93b07fd8a",
"1000",
"4d465f1c-0507-5dfa-9ea0-e2de1a9e90a5",
),
# Different user on the same machine.
(
"09e28913cb074ed995a239c93b07fd8a",
"1001",
"9f522470-d57d-55e2-8f74-b90b19830e9d",
),
# Same effective user id, but in a different machine
(
"771640198a6344bba7ad356cf525243a",
"1000",
"b4e5eb85-c750-5130-bd72-851e531e73c3",
),
),
)
def test_generate_session_id_different_users(
tmp_path, machine_id, effective_user_id, expected
):
machine_id_file = tmp_path / "machine-id"
machine_id_file.write_text(machine_id)
with patch(
"command_line_assistant.daemon.session.MACHINE_ID_PATH", machine_id_file
):
session = UserSessionManager(effective_user_id)
assert session.session_id == uuid.UUID(expected)


def test_empty_machine_id_file(tmp_path):
machine_id_file = tmp_path / "machine-id"
machine_id_file.write_text("")
with patch(
"command_line_assistant.daemon.session.MACHINE_ID_PATH", machine_id_file
):
session = UserSessionManager("1000")
with pytest.raises(ValueError, match="Machine ID at .* is empty"):
assert session.machine_id


def test_machine_id_file_not_found(tmp_path):
machine_id_file = tmp_path / "test" / "machine-id"
with patch(
"command_line_assistant.daemon.session.MACHINE_ID_PATH", machine_id_file
):
session = UserSessionManager("1000")
with pytest.raises(FileNotFoundError, match="Machine ID file not found at .*"):
assert session.machine_id

0 comments on commit d8ccf9e

Please sign in to comment.