# SPDX-FileCopyrightText: (C) 2025 NetKnights GmbH <https://netknights.it>
# SPDX-FileCopyrightText: (C) 2025 Paul Lettich <paul.lettich@netknights.it>
# SPDX-FileCopyrightText: (C) 2024 Jelina Unger <jelina.unger@netknights.it>
# SPDX-FileCopyrightText: (C) 2024 Nils Behlen <nils.behlen@netknights.it>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from datetime import datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from privacyidea.models.token import Token
from sqlalchemy import Unicode, Integer, Boolean, DateTime, UniqueConstraint, select, UnicodeText
from sqlalchemy.orm import Mapped, mapped_column, relationship
from privacyidea.lib.utils import convert_column_to_unicode
from privacyidea.models import db
from privacyidea.models.realm import Realm
from privacyidea.models.utils import MethodsMixin
log = logging.getLogger(__name__)
[docs]
class TokenContainer(MethodsMixin, db.Model):
"""
The "Tokencontainer" table contains the containers and their associated tokens.
"""
__tablename__ = 'tokencontainer'
id: Mapped[int] = mapped_column("id", Integer, primary_key=True)
type: Mapped[str] = mapped_column(Unicode(100), default='Generic', nullable=False)
description: Mapped[str | None] = mapped_column(Unicode(1024), default='')
serial: Mapped[str] = mapped_column(Unicode(40), default='', unique=True, nullable=False, index=True)
last_seen: Mapped[datetime | None] = mapped_column(DateTime, default=None)
last_updated: Mapped[datetime | None] = mapped_column(DateTime, default=None)
template_id: Mapped[int | None] = mapped_column(Integer, db.ForeignKey('tokencontainertemplate.id',
name="tokencontainertemplate_id"))
tokens: Mapped[list['Token']] = relationship(secondary='tokencontainertoken', back_populates='container')
owners: Mapped[list['TokenContainerOwner']] = relationship(lazy='dynamic', back_populates='container',
cascade="all, delete-orphan")
states: Mapped[list['TokenContainerStates']] = relationship(lazy='dynamic', back_populates='container',
cascade="all, delete-orphan")
info_list: Mapped[list['TokenContainerInfo']] = relationship(lazy='select', back_populates='container',
cascade="all, delete-orphan")
realms: Mapped[list['Realm']] = relationship(secondary='tokencontainerrealm', back_populates='container')
template: Mapped['TokenContainerTemplate'] = relationship(back_populates='containers')
def __init__(self, serial: str, container_type: str = "Generic", tokens: list['Token'] | None = None,
description: str = "", states: list['TokenContainerStates'] | None = None):
self.serial = serial
self.type = container_type
self.description = description
if tokens:
# Assumes the tokens list contains Token objects
self.tokens = tokens
if states:
self.states = states
[docs]
class TokenContainerOwner(MethodsMixin, db.Model):
__tablename__ = 'tokencontainerowner'
id: Mapped[int] = mapped_column("id", Integer, primary_key=True)
container_id: Mapped[int | None] = mapped_column(Integer, db.ForeignKey("tokencontainer.id"))
resolver: Mapped[str | None] = mapped_column(Unicode(120), default='', index=True)
user_id: Mapped[str | None] = mapped_column(Unicode(320), default='', index=True)
realm_id: Mapped[int | None] = mapped_column(Integer, db.ForeignKey('realm.id'))
container: Mapped['TokenContainer'] = relationship(back_populates='owners')
realm: Mapped['Realm'] = relationship(lazy='joined', backref='tokencontainerowners')
def __init__(self, container_id: int | None = None, container_serial: str | None = None,
resolver: str | None = None, user_id: str | None = None, realm_id: int | None = None,
realm_name: str | None = None):
"""
Create a new TokenContainerOwner assignment.
"""
if realm_id is not None:
self.realm_id = realm_id
elif realm_name:
stmt = select(Realm).filter_by(name=realm_name)
realm = db.session.execute(stmt).scalar_one_or_none()
self.realm_id = realm.id if realm else None
if container_id is not None:
self.container_id = container_id
elif container_serial:
stmt = select(TokenContainer).filter_by(serial=container_serial)
container = db.session.execute(stmt).scalar_one_or_none()
self.container_id = container.id if container else None
self.resolver = resolver
self.user_id = user_id
[docs]
class TokenContainerStates(MethodsMixin, db.Model):
__tablename__ = 'tokencontainerstates'
id: Mapped[int] = mapped_column("id", Integer, primary_key=True)
container_id: Mapped[int | None] = mapped_column(Integer, db.ForeignKey("tokencontainer.id"))
state: Mapped[str] = mapped_column(Unicode(100), default='active', nullable=False)
container: Mapped['TokenContainer'] = relationship("TokenContainer", back_populates="states")
def __init__(self, container_id: int | None = None, state: str = "active"):
self.container_id = container_id
self.state = state
[docs]
class TokenContainerInfo(MethodsMixin, db.Model):
"""
The table "tokencontainerinfo" is used to store additional, long information that
is specific to the containertype.
"""
__tablename__ = 'tokencontainerinfo'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
key: Mapped[str] = mapped_column(Unicode(255), nullable=False)
value: Mapped[str | None] = mapped_column(UnicodeText(), default='')
type: Mapped[str | None] = mapped_column(Unicode(100), default='')
description: Mapped[str | None] = mapped_column(Unicode(2000), default='')
container_id: Mapped[int | None] = mapped_column(Integer, db.ForeignKey('tokencontainer.id'), index=True)
container: Mapped['TokenContainer'] = relationship('TokenContainer', back_populates='info_list')
__table_args__ = (UniqueConstraint('container_id', 'key', name='container_id_constraint'),)
def __init__(self, container_id: int, key: str, value: str, type: str | None = None,
description: str | None = None):
"""
Create a new tokencontainerinfo for a given token_id
"""
self.container_id = container_id
self.key = key
self.value = convert_column_to_unicode(value)
self.type = type
self.description = description
[docs]
class TokenContainerRealm(MethodsMixin, db.Model):
"""
This table stores to which realms a container is assigned.
"""
__tablename__ = 'tokencontainerrealm'
container_id: Mapped[int] = mapped_column(Integer, db.ForeignKey("tokencontainer.id"), primary_key=True)
realm_id: Mapped[int] = mapped_column(Integer, db.ForeignKey('realm.id'), primary_key=True)
[docs]
class TokenContainerTemplate(MethodsMixin, db.Model):
__tablename__ = 'tokencontainertemplate'
id: Mapped[int] = mapped_column("id", Integer, primary_key=True)
options: Mapped[str | None] = mapped_column(Unicode(2000), default='')
name: Mapped[str | None] = mapped_column(Unicode(200), default='')
container_type: Mapped[str] = mapped_column(Unicode(100), default='generic', nullable=False)
default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
containers: Mapped[list['TokenContainer']] = relationship('TokenContainer', back_populates='template')
def __init__(self, name: str, container_type: str = "generic", options: str = '', default: bool = False):
self.name = name
self.container_type = container_type
self.options = options
self.default = default
[docs]
class TokenContainerToken(MethodsMixin, db.Model):
"""
Association table to link tokens to containers.
"""
__tablename__ = 'tokencontainertoken'
token_id: Mapped[int] = mapped_column('token_id', Integer, db.ForeignKey('token.id'), primary_key=True)
container_id: Mapped[int] = mapped_column('container_id', Integer, db.ForeignKey('tokencontainer.id'),
primary_key=True)