Skip to content

Auth

pytruenas.auth

ApiKeyAuth(api_key, username=None)

Bases: Credentials

Source code in src/pytruenas/auth.py
239
240
241
242
243
244
def __init__(self, api_key: str, username: str | None = None) -> None:
    self.api_key = api_key
    # login_ex's API_KEY_PLAIN wants the username too; the legacy
    # login_with_api_key does not. If unknown, the modern path can't be used
    # and login_ex falls back to legacy.
    self.username = username

MECHANISM = 'API_KEY_PLAIN' class-attribute instance-attribute

METHOD = 'login_with_api_key' class-attribute instance-attribute

api_key = api_key instance-attribute

username = username instance-attribute

AuthenticationError(response_type, response)

Bases: Exception

A modern auth.login_ex login was refused.

response_type is the server's discriminator (AUTH_ERR/DENIED/ EXPIRED/…); response is the full response dict for callers that need the detail (e.g. a REDIRECT target).

Source code in src/pytruenas/auth.py
82
83
84
85
def __init__(self, response_type: str, response: dict) -> None:
    super().__init__(f"login_ex failed: {response_type}")
    self.response_type = response_type
    self.response = response

response = response instance-attribute

response_type = response_type instance-attribute

BasicAuth(username, password, otp_token=None)

Bases: Credentials

Source code in src/pytruenas/auth.py
277
278
279
280
281
282
def __init__(
    self, username: str, password: str, otp_token: str | None = None
) -> None:
    self.username = username
    self.password = password
    self.otp_token = otp_token

MECHANISM = 'PASSWORD_PLAIN' class-attribute instance-attribute

METHOD = 'login' class-attribute instance-attribute

otp_token = otp_token instance-attribute

password = password instance-attribute

username = username instance-attribute

Credentials(*args, **kwargs)

Source code in src/pytruenas/auth.py
97
98
def __init__(self, *args, **kwargs) -> None:
    pass

MECHANISM = None class-attribute instance-attribute

METHOD = None class-attribute instance-attribute

from_env(env=None) staticmethod

Source code in src/pytruenas/auth.py
167
168
169
170
171
172
173
@staticmethod
def from_env(env: _ty.Mapping | None = None):
    if env is None:
        import os

        env = os.environ
    return Credentials(env.get("TN_CREDS"))

from_host_credentials(username=None, password=None, otp=None, api_key=None, token=None) staticmethod

Select a credential from an already-parsed credential mapping.

This is the hostctl bridge. hostctl.host.parse_credentials has already split a URI's password field into the password proper and its trailing key:value extras -- the OTP arrives here as otp, not embedded in password. So this method deliberately does no string parsing: it only picks the matching subclass. Contrast Credentials(...), which still accepts (and must keep accepting) the raw single-string form used by TN_CREDS and the creds= argument.

Both projects encode an OTP the same way -- a newline after the password, then otp:<token> -- so the two paths agree on meaning while splitting the string exactly once, here or in hostctl, never twice.

A newline is the separator because a password can never contain one: at an interactive prompt, Enter submits the value rather than being typed into it. That makes the split free -- no character is stolen from the password alphabet and no escaping scheme is needed. The one caveat is transport-specific: a URI cannot carry a raw newline (urlsplit strips it), so an OTP in a connection string must be percent-encoded as %0A. See :mod:pytruenas.host.

Precedence when several are supplied: token, then api_key, then password. They are mutually exclusive mechanisms rather than a fallback chain, so passing more than one is a caller error; the order only makes the outcome deterministic rather than arbitrary. With none of them, this is local-socket auth (:class:LocalAuth), which is also what an empty mapping means for a local target.

Source code in src/pytruenas/auth.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
@staticmethod
def from_host_credentials(
    username: "str | None" = None,
    password: "str | None" = None,
    otp: "str | None" = None,
    api_key: "str | None" = None,
    token: "str | None" = None,
) -> "Credentials":
    """Select a credential from an already-parsed credential mapping.

    This is the ``hostctl`` bridge. ``hostctl.host.parse_credentials`` has
    already split a URI's password field into the password proper and its
    trailing ``key:value`` extras -- the OTP arrives here as ``otp``, not
    embedded in ``password``. So this method deliberately does **no string
    parsing**: it only picks the matching subclass. Contrast
    ``Credentials(...)``, which still accepts (and must keep accepting) the
    raw single-string form used by ``TN_CREDS`` and the ``creds=`` argument.

    Both projects encode an OTP the same way -- a newline after the
    password, then ``otp:<token>`` -- so the two paths agree on meaning
    while splitting the string exactly once, here or in hostctl, never
    twice.

    A newline is the separator because **a password can never contain one**:
    at an interactive prompt, Enter submits the value rather than being
    typed into it. That makes the split free -- no character is stolen from
    the password alphabet and no escaping scheme is needed. The one caveat
    is transport-specific: a *URI* cannot carry a raw newline (``urlsplit``
    strips it), so an OTP in a connection string must be percent-encoded as
    ``%0A``. See :mod:`pytruenas.host`.

    Precedence when several are supplied: ``token``, then ``api_key``, then
    ``password``. They are mutually exclusive mechanisms rather than a
    fallback chain, so passing more than one is a caller error; the order
    only makes the outcome deterministic rather than arbitrary. With none of
    them, this is local-socket auth (:class:`LocalAuth`), which is also what
    an empty mapping means for a local target.
    """
    if token:
        return TokenAuth(token)
    if api_key:
        return ApiKeyAuth(api_key, username)
    if password:
        # username defaults to root to match the rest of the stack: hostctl's
        # SshConfig and pytruenas' own shell handling both treat a missing
        # user as root, and BasicAuth requires one positionally.
        return BasicAuth(username or "root", password, otp)
    # An OTP with nothing to attach it to cannot authenticate anything, and
    # silently downgrading to LocalAuth would turn a typo into a confusing
    # "local socket unavailable" much later. Fail where the mistake is.
    if otp:
        raise ValueError("otp requires a password")
    return LocalAuth()

login(client)

Legacy login via auth.login/login_with_* (unchanged).

Source code in src/pytruenas/auth.py
103
104
105
106
107
def login(self, client: "TrueNASClient"):
    """Legacy login via ``auth.login``/``login_with_*`` (unchanged)."""
    if self.METHOD:
        return client.api.auth[self.METHOD](*self._args())
    return None

login_ex(client, *, login_options=None, otp_provider=None)

Authenticate via the modern auth.login_ex mechanism.

Handles the discriminated response: SUCCESS returns the response dict; OTP_REQUIRED calls auth.login_ex_continue with an OTP token (from this credential's own otp_token if set, else from otp_provider() if given, else raises); any other response_type raises :class:AuthenticationError. A credential with no login_ex mechanism (e.g. :class:LocalAuth) falls back to legacy :meth:login.

login_options overrides the server defaults ({"user_info": True, "reconnect_token": False}).

Source code in src/pytruenas/auth.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def login_ex(
    self,
    client: "TrueNASClient",
    *,
    login_options: "dict | None" = None,
    otp_provider: "_ty.Callable[[], str] | None" = None,
) -> "dict | None":
    """Authenticate via the modern ``auth.login_ex`` mechanism.

    Handles the discriminated response: ``SUCCESS`` returns the response
    dict; ``OTP_REQUIRED`` calls ``auth.login_ex_continue`` with an OTP
    token (from this credential's own ``otp_token`` if set, else from
    ``otp_provider()`` if given, else raises); any other ``response_type``
    raises :class:`AuthenticationError`. A credential with no login_ex
    mechanism (e.g. :class:`LocalAuth`) falls back to legacy :meth:`login`.

    ``login_options`` overrides the server defaults (``{"user_info": True,
    "reconnect_token": False}``).
    """
    data = self._login_data()
    if data is None:  # no login_ex form -> legacy path
        self.login(client)
        return None
    if login_options is not None:
        data = {**data, "login_options": login_options}

    resp = client.api.auth.login_ex(data)
    resp = _ty.cast(dict, resp) or {}
    rtype = resp.get("response_type")

    if rtype == "OTP_REQUIRED":
        otp = getattr(self, "otp_token", None)
        if not otp and otp_provider is not None:
            otp = otp_provider()
        if not otp:
            raise AuthenticationError("OTP_REQUIRED", resp)
        resp = (
            _ty.cast(
                dict,
                client.api.auth.login_ex_continue(
                    {"mechanism": "OTP_TOKEN", "otp_token": otp}
                ),
            )
            or {}
        )
        rtype = resp.get("response_type")

    if rtype == "SUCCESS":
        return resp
    raise AuthenticationError(rtype or "UNKNOWN", resp)

LocalAuth(*args, **kwargs)

Bases: Credentials

Source code in src/pytruenas/auth.py
97
98
def __init__(self, *args, **kwargs) -> None:
    pass

TokenAuth(token)

Bases: Credentials

Source code in src/pytruenas/auth.py
263
264
def __init__(self, token: str) -> None:
    self.token = token

MECHANISM = 'TOKEN_PLAIN' class-attribute instance-attribute

METHOD = 'login_with_token' class-attribute instance-attribute

token = token instance-attribute