Skip to content

Security

SecurityNamespace for security group management and service account utilities, accessed as client.security.

security

Security group utilities for Vodoo.

AccessDefinition dataclass

AccessDefinition(model: str, perm_read: bool, perm_write: bool, perm_create: bool, perm_unlink: bool)

Access control entry for a model.

RuleDefinition dataclass

RuleDefinition(model: str, domain: str, perm_read: bool, perm_write: bool, perm_create: bool, perm_unlink: bool)

Record rule definition for a model.

GroupDefinition dataclass

GroupDefinition(name: str, comment: str, access: tuple[AccessDefinition, ...], rules: tuple[RuleDefinition, ...] = ())

Security group definition.

SecurityNamespace

SecurityNamespace(client: OdooClient)

Security group operations namespace.

Source code in src/vodoo/security.py
def __init__(self, client: OdooClient) -> None:
    self._client = client

create_groups

create_groups() -> tuple[dict[str, int], list[str]]

Create (or reuse) all Vodoo security groups.

RETURNS DESCRIPTION
tuple[dict[str, int], list[str]]

Tuple of group name -> group ID and a list of warnings.

Source code in src/vodoo/security.py
def create_groups(self) -> tuple[dict[str, int], list[str]]:
    """Create (or reuse) all Vodoo security groups.

    Returns:
        Tuple of group name -> group ID and a list of warnings.

    """
    warnings: list[str] = []
    group_ids: dict[str, int] = {}

    for group in GROUP_DEFINITIONS:
        group_id = self._ensure_group(group)
        group_ids[group.name] = group_id

        for access in group.access:
            model_id = self._get_model_id(access.model)
            if model_id is None:
                warnings.append(f"Model '{access.model}' not found; skipping access")
                continue
            self._ensure_access(group_id, group.name, model_id, access)

        for rule in group.rules:
            model_id = self._get_model_id(rule.model)
            if model_id is None:
                warnings.append(f"Model '{rule.model}' not found; skipping rule")
                continue
            self._ensure_rule(group_id, group.name, model_id, rule)

    return group_ids, warnings

get_group_ids

get_group_ids(group_names: list[str]) -> tuple[dict[str, int], list[str]]

Fetch group IDs for the provided names.

PARAMETER DESCRIPTION
group_names

Group names to resolve

TYPE: list[str]

RETURNS DESCRIPTION
tuple[dict[str, int], list[str]]

Tuple of group name -> group ID and a list of warnings.

Source code in src/vodoo/security.py
def get_group_ids(
    self,
    group_names: list[str],
) -> tuple[dict[str, int], list[str]]:
    """Fetch group IDs for the provided names.

    Args:
        group_names: Group names to resolve

    Returns:
        Tuple of group name -> group ID and a list of warnings.

    """
    warnings: list[str] = []
    group_ids: dict[str, int] = {}

    for name in group_names:
        ids = self._client.search("res.groups", domain=[("name", "=", name)], limit=1)
        if ids:
            group_ids[name] = ids[0]
        else:
            warnings.append(f"Group '{name}' not found")

    return group_ids, warnings

assign

assign(user_id: int, group_ids: list[int], *, remove_default_groups: bool = True) -> None

Assign a user to the provided groups.

PARAMETER DESCRIPTION
user_id

User ID to update

TYPE: int

group_ids

Group IDs to add

TYPE: list[int]

remove_default_groups

If True, remove base.group_user and base.group_portal first

TYPE: bool DEFAULT: True

Source code in src/vodoo/security.py
def assign(
    self,
    user_id: int,
    group_ids: list[int],
    *,
    remove_default_groups: bool = True,
) -> None:
    """Assign a user to the provided groups.

    Args:
        user_id: User ID to update
        group_ids: Group IDs to add
        remove_default_groups: If True, remove base.group_user and base.group_portal first

    """
    commands: list[tuple[int, int]] = []

    if remove_default_groups:
        for xmlid in ("base.group_user", "base.group_portal"):
            group_id = self._get_group_id_by_xmlid(xmlid)
            if group_id is not None:
                commands.append((3, group_id))

    commands.extend((4, group_id) for group_id in group_ids)

    self._client.write("res.users", [user_id], {self._groups_field(): commands})

resolve_user

resolve_user(*, user_id: int | None = None, login: str | None = None) -> int

Resolve a user ID from either an ID or login name.

PARAMETER DESCRIPTION
user_id

Explicit user ID

TYPE: int | None DEFAULT: None

login

User login/email

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
int

User ID

RAISES DESCRIPTION
ValueError

If user not found

Source code in src/vodoo/security.py
def resolve_user(self, *, user_id: int | None = None, login: str | None = None) -> int:
    """Resolve a user ID from either an ID or login name.

    Args:
        user_id: Explicit user ID
        login: User login/email

    Returns:
        User ID

    Raises:
        ValueError: If user not found

    """
    if user_id is not None:
        return user_id
    if not login:
        raise ValueError("Provide --user-id or --login")

    ids = self._client.search("res.users", domain=[("login", "=", login)], limit=1)
    if not ids:
        raise ValueError(f"User with login '{login}' not found")
    return ids[0]

create_user

create_user(name: str, login: str, password: str | None = None, email: str | None = None) -> tuple[int, str]

Create a new user.

PARAMETER DESCRIPTION
name

User's display name

TYPE: str

login

User's login (usually email)

TYPE: str

password

User's password (generated if not provided)

TYPE: str | None DEFAULT: None

email

User's email (defaults to login if not provided)

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
tuple[int, str]

Tuple of (user_id, password)

Source code in src/vodoo/security.py
def create_user(
    self,
    name: str,
    login: str,
    password: str | None = None,
    email: str | None = None,
) -> tuple[int, str]:
    """Create a new user.

    Args:
        name: User's display name
        login: User's login (usually email)
        password: User's password (generated if not provided)
        email: User's email (defaults to login if not provided)

    Returns:
        Tuple of (user_id, password)

    """
    # Generate password if not provided
    if password is None:
        password = _generate_password()

    # Use login as email if not provided
    if email is None:
        email = login

    # Create user with no groups (share user, not billed)
    user_id = self._client.create(
        "res.users",
        {
            "name": name,
            "login": login,
            "email": email,
            "password": password,
            self._groups_field(): [(6, 0, [])],  # Empty groups = share user
        },
    )

    return user_id, password

set_password

set_password(user_id: int, password: str | None = None) -> str

Set a user's password.

PARAMETER DESCRIPTION
user_id

User ID

TYPE: int

password

New password (generated if not provided)

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
str

The password that was set

Source code in src/vodoo/security.py
def set_password(
    self,
    user_id: int,
    password: str | None = None,
) -> str:
    """Set a user's password.

    Args:
        user_id: User ID
        password: New password (generated if not provided)

    Returns:
        The password that was set

    """
    # Generate password if not provided
    if password is None:
        password = _generate_password()

    self._client.write("res.users", [user_id], {"password": password})
    return password

get_user

get_user(user_id: int) -> dict[str, Any]

Get user information.

PARAMETER DESCRIPTION
user_id

User ID

TYPE: int

RETURNS DESCRIPTION
dict[str, Any]

User information dictionary

Source code in src/vodoo/security.py
def get_user(self, user_id: int) -> dict[str, Any]:
    """Get user information.

    Args:
        user_id: User ID

    Returns:
        User information dictionary

    """
    users = self._client.search_read(
        "res.users",
        domain=[("id", "=", user_id)],
        fields=[
            "name",
            "login",
            "email",
            "active",
            "share",
            self._groups_field(),
            "partner_id",
        ],
        limit=1,
    )
    if not users:
        raise ValueError(f"User {user_id} not found")
    return users[0]