Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NAS-129889 / 24.10 / Fix schema for directory services users #13972

Merged
merged 2 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions src/middlewared/middlewared/api/base/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,47 @@
from pydantic.functional_validators import AfterValidator
from typing_extensions import Annotated

__all__ = ["LocalUsername", "LocalUID"]
__all__ = ["LocalUsername", "RemoteUsername", "LocalUID"]

TRUENAS_IDMAP_DEFAULT_LOW = 90000001

DEFAULT_VALID_CHARS = string.ascii_letters + string.digits + '_' + '-' + '$' + '.'
DEFAULT_VALID_START = string.ascii_letters + '_'
DEFAULT_MAX_LENGTH = 32

def validate_local_username(val):
# see man 8 useradd, specifically the CAVEATS section
# NOTE: we are ignoring the man page's recommendation for insistence
# upon the starting character of a username be a lower-case letter.
# We aren't enforcing this for maximum backwards compatibility

def validate_username(
val: str,
valid_chars: str = DEFAULT_VALID_CHARS,
valid_start_chars : str | None = DEFAULT_VALID_START,
max_length: int | None = DEFAULT_MAX_LENGTH
):
val_len = len(val)
valid_chars = string.ascii_letters + string.digits + '_' + '-' + '$' + '.'
valid_start = string.ascii_letters + '_'
assert val_len > 0, 'Username must be at least 1 character in length'
assert val_len <= 32, 'Username cannot exceed 32 characters in length'
assert val[0] in valid_start, 'Username must start with a letter or an underscore'
if max_length is not None:
assert val_len <= max_length, f'Username cannot exceed {max_length} charaters in length'
if valid_start_chars is not None:
assert val[0] in valid_start_chars, 'Username must start with a letter or an underscore'

assert '$' not in val or val[-1] == '$', 'Username must end with a dollar sign character'
assert all(char in valid_chars for char in val), f'Valid characters for a username are: {", ".join(valid_chars)!r}'
return val


def validate_local_username(val):
yocalebo marked this conversation as resolved.
Show resolved Hide resolved
# see man 8 useradd, specifically the CAVEATS section
# NOTE: we are ignoring the man page's recommendation for insistence
# upon the starting character of a username be a lower-case letter.
# We aren't enforcing this for maximum backwards compatibility
return validate_username(val)


def validate_remote_username(val):
# Restrictions on names returned by nss_winbind are more lax than we place
# on our local usernames. \\ is used as a separator for domain and username
return validate_username(val, DEFAULT_VALID_CHARS + '\\', None, None)


LocalUsername = Annotated[str, AfterValidator(validate_local_username)]
RemoteUsername = Annotated[str, AfterValidator(validate_remote_username)]
LocalUID = Annotated[int, Ge(0), Le(TRUENAS_IDMAP_DEFAULT_LOW - 1)]
10 changes: 5 additions & 5 deletions src/middlewared/middlewared/api/v25_04_0/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from pydantic import EmailStr
from typing_extensions import Annotated

from middlewared.api.base import (BaseModel, Excluded, excluded_field, ForUpdateMetaclass, LocalUsername, LocalUID,
LongString, NonEmptyString, Private, single_argument_result)
from middlewared.api.base import (BaseModel, Excluded, excluded_field, ForUpdateMetaclass, LocalUsername, RemoteUsername,
LocalUID, LongString, NonEmptyString, Private, single_argument_result)

__all__ = ["UserEntry", "UserCreateArgs", "UserCreateResult", "UserUpdateArgs", "UserUpdateResult",
"UserRenew2faSecretArgs", "UserRenew2faSecretResult"]
Expand All @@ -15,9 +15,9 @@
class UserEntry(BaseModel):
id: int
uid: int
username: LocalUsername
unixhash: Private[str]
smbhash: Private[str]
username: LocalUsername | RemoteUsername
unixhash: Private[str | None]
smbhash: Private[str | None]
home: NonEmptyString = DEFAULT_HOME_PATH
shell: NonEmptyString = "/usr/bin/zsh"
"Available choices can be retrieved with `user.shell_choices`."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,15 @@ def fill_cache(
'shell': user_data.pw_shell,
'full_name': user_data.pw_gecos,
'builtin': False,
'email': '',
'email': None,
'password_disabled': False,
'locked': False,
'sudo_commands': [],
'sudo_commands_nopasswd': False,
'attributes': {},
'sudo_commands_nopasswd': [],
'groups': [],
'sshpubkey': None,
'immutable': True,
'two_factor_auth_configured': False,
'twofactor_auth_configured': False,
'local': False,
'id_type_both': id_type_both,
'nt_name': user_data.pw_name,
Expand Down
6 changes: 3 additions & 3 deletions src/middlewared/middlewared/plugins/idmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -1174,11 +1174,11 @@ async def synthetic_user(self, passwd, sid):
'unixhash': None,
'smbhash': None,
'group': {},
'home': '',
'shell': '',
'home': passwd['pw_dir'],
'shell': passwd['pw_shell'],
'full_name': passwd['pw_gecos'],
'builtin': False,
'email': '',
'email': None,
'password_disabled': False,
'locked': False,
'sudo_commands': [],
Expand Down
Loading