diff --git a/src/middlewared/middlewared/api/base/types/user.py b/src/middlewared/middlewared/api/base/types/user.py index eda72977f1a9e..1d29b447ea568 100644 --- a/src/middlewared/middlewared/api/base/types/user.py +++ b/src/middlewared/middlewared/api/base/types/user.py @@ -4,7 +4,9 @@ from pydantic.functional_validators import AfterValidator from typing_extensions import Annotated -__all__ = ["LocalUsername", "RemoteUsername", "LocalUID"] +from middlewared.utils.sid import sid_is_valid + +__all__ = ["LocalUsername", "RemoteUsername", "LocalUID", "LocalGID", "SID"] TRUENAS_IDMAP_DEFAULT_LOW = 90000001 @@ -45,6 +47,21 @@ def validate_remote_username(val: str) -> str: return validate_username(val, DEFAULT_VALID_CHARS + '\\', None, None) +def validate_sid(value: str) -> str: + value = value.strip() + value = value.upper() + + assert sid_is_valid(value), ('SID is malformed. See MS-DTYP Section 2.4 for SID type specifications. Typically ' + 'SIDs refer to existing objects on the local or remote server and so an appropriate ' + 'value should be queried prior to submitting to API endpoints.') + + return value + + 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)] + +LocalGID = Annotated[int, Ge(0), Le(TRUENAS_IDMAP_DEFAULT_LOW - 1)] + +SID = Annotated[str, AfterValidator(validate_sid)] diff --git a/src/middlewared/middlewared/api/v25_04_0/__init__.py b/src/middlewared/middlewared/api/v25_04_0/__init__.py index 4e51c596d68b0..132eefb9f40f1 100644 --- a/src/middlewared/middlewared/api/v25_04_0/__init__.py +++ b/src/middlewared/middlewared/api/v25_04_0/__init__.py @@ -1,6 +1,9 @@ from .api_key import * # noqa +from .auth import * # noqa from .cloud_sync import * # noqa from .common import * # noqa from .core import * # noqa +from .group import * # noqa +from .privilege import * # noqa from .user import * # noqa from .vendor import * # noqa diff --git a/src/middlewared/middlewared/api/v25_04_0/auth.py b/src/middlewared/middlewared/api/v25_04_0/auth.py new file mode 100644 index 0000000000000..33f9716917f23 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/auth.py @@ -0,0 +1,14 @@ +from middlewared.api.base import BaseModel, single_argument_result +from .user import UserGetUserObjResult + + +class AuthMeArgs(BaseModel): + pass + + +@single_argument_result +class AuthMeResult(UserGetUserObjResult.model_fields["result"].annotation): + attributes: dict + two_factor_config: dict + privilege: dict + account_attributes: list[str] diff --git a/src/middlewared/middlewared/api/v25_04_0/group.py b/src/middlewared/middlewared/api/v25_04_0/group.py new file mode 100644 index 0000000000000..9f474e0d621e1 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/group.py @@ -0,0 +1,129 @@ +from typing import Literal + +from annotated_types import Ge, Le +from pydantic import Field +from typing_extensions import Annotated + +from middlewared.api.base import (BaseModel, Excluded, excluded_field, ForUpdateMetaclass, LocalUsername, RemoteUsername, + LocalUID, LongString, NonEmptyString, Private, single_argument_args, single_argument_result) + +__all__ = ["GroupEntry", + "GroupCreateArgs", "GroupCreateResult", + "GroupUpdateArgs", "GroupUpdateResult", + "GroupDeleteArgs", "GroupDeleteResult", + "GroupGetNextGidArgs", "GroupGetNextGidResult", + "GroupGetGroupObjArgs", "GroupGetGroupObjResult", + "GroupHasPasswordEnabledUserArgs", "GroupHasPasswordEnabledUserResult"] + + +class GroupEntry(BaseModel): + id: int + gid: int + name: NonEmptyString + builtin: bool + sudo_commands: list[NonEmptyString] = [] + sudo_commands_nopasswd: list[NonEmptyString] = [] + smb: bool = True + "Specifies whether the group should be mapped into an NT group." + group: NonEmptyString + id_type_both: bool + local: bool + sid: str | None + roles: list[str] + users: list[int] = [] + "A list of user ids (`id` attribute from `user.query`)." + + +class GroupCreate(GroupEntry): + id: Excluded = excluded_field() + builtin: Excluded = excluded_field() + group: Excluded = excluded_field() + id_type_both: Excluded = excluded_field() + local: Excluded = excluded_field() + sid: Excluded = excluded_field() + roles: Excluded = excluded_field() + + gid: LocalUID | None = None + "If `null`, it is automatically filled with the next one available." + allow_duplicate_gid: bool = False + "Allows distinct group names to share the same gid." + + +class GroupCreateArgs(BaseModel): + group_create: GroupCreate + + +class GroupCreateResult(BaseModel): + result: int + + +class GroupUpdate(GroupCreate, metaclass=ForUpdateMetaclass): + pass + + +class GroupUpdateArgs(BaseModel): + id: int + group_update: GroupUpdate + + +class GroupUpdateResult(BaseModel): + result: int + + +class GroupDeleteOptions(BaseModel): + delete_users: bool = False + "Deletes all users that have this group as their primary group." + + +class GroupDeleteArgs(BaseModel): + id: int + options: GroupDeleteOptions = Field(default=GroupDeleteOptions()) + + +class GroupDeleteResult(BaseModel): + result: int + + +class GroupGetNextGidArgs(BaseModel): + pass + + +class GroupGetNextGidResult(BaseModel): + result: int + + +@single_argument_args("get_group_obj") +class GroupGetGroupObjArgs(BaseModel): + groupname: str | None = None + gid: int | None = None + sid_info: bool = False + + +@single_argument_result +class GroupGetGroupObjResult(BaseModel): + gr_name: str + "name of the group" + gr_gid: int + "group id of the group" + gr_mem: list[int] + "list of gids that are members of the group" + sid: str | None = None + "optional SID value for the account that is present if `sid_info` is specified in payload." + source: Literal['LOCAL', 'ACTIVEDIRECTORY', 'LDAP'] + """ + the name server switch module that provided the user. Options are: + FILES - local user in passwd file of server, + WINBIND - user provided by winbindd, + SSS - user provided by SSSD. + """ + local: bool + "boolean indicating whether this group is local to the NAS or provided by a directory service." + + +class GroupHasPasswordEnabledUserArgs(BaseModel): + gids: list[int] + exclude_user_ids: list[int] = [] + + +class GroupHasPasswordEnabledUserResult(BaseModel): + result: bool diff --git a/src/middlewared/middlewared/api/v25_04_0/privilege.py b/src/middlewared/middlewared/api/v25_04_0/privilege.py new file mode 100644 index 0000000000000..92cc53dad99bf --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/privilege.py @@ -0,0 +1,55 @@ +from middlewared.api.base import BaseModel, Excluded, excluded_field, ForUpdateMetaclass, NonEmptyString, SID +from .api_key import AllowListItem +from .group import GroupEntry + +__all__ = ["PrivilegeEntry", + "PrivilegeCreateArgs", "PrivilegeCreateResult", + "PrivilegeUpdateArgs", "PrivilegeUpdateResult", + "PrivilegeDeleteArgs", "PrivilegeDeleteResult"] + + +class PrivilegeEntry(BaseModel): + id: int + builtin_name: str | None + name: NonEmptyString + local_groups: list[GroupEntry] + ds_groups: list[GroupEntry] + allowlist: list[AllowListItem] = [] + roles: list[str] = [] + web_shell: bool + + +class PrivilegeCreate(PrivilegeEntry): + id: Excluded = excluded_field() + builtin_name: Excluded = excluded_field() + local_groups: list[int] = [] + ds_groups: list[int | SID] = [] + + +class PrivilegeCreateArgs(BaseModel): + privilege_create: PrivilegeCreate + + +class PrivilegeCreateResult(BaseModel): + result: PrivilegeEntry + + +class PrivilegeUpdate(PrivilegeCreate, metaclass=ForUpdateMetaclass): + pass + + +class PrivilegeUpdateArgs(BaseModel): + id: int + privilege_update: PrivilegeUpdate + + +class PrivilegeUpdateResult(BaseModel): + result: PrivilegeEntry + + +class PrivilegeDeleteArgs(BaseModel): + id: int + + +class PrivilegeDeleteResult(BaseModel): + result: bool diff --git a/src/middlewared/middlewared/api/v25_04_0/user.py b/src/middlewared/middlewared/api/v25_04_0/user.py index 9326637f40d10..a1fdd877df1f8 100644 --- a/src/middlewared/middlewared/api/v25_04_0/user.py +++ b/src/middlewared/middlewared/api/v25_04_0/user.py @@ -1,11 +1,27 @@ +from typing import Literal + from annotated_types import Ge, Le -from pydantic import EmailStr +from pydantic import EmailStr, Field from typing_extensions import Annotated from middlewared.api.base import (BaseModel, Excluded, excluded_field, ForUpdateMetaclass, LocalUsername, RemoteUsername, - LocalUID, LongString, NonEmptyString, Private, single_argument_result) + LocalUID, LongString, NonEmptyString, Private, single_argument_args, + single_argument_result) -__all__ = ["UserEntry", "UserCreateArgs", "UserCreateResult", "UserUpdateArgs", "UserUpdateResult", +__all__ = ["UserEntry", + "UserCreateArgs", "UserCreateResult", + "UserUpdateArgs", "UserUpdateResult", + "UserDeleteArgs", "UserDeleteResult", + "UserShellChoicesArgs", "UserShellChoicesResult", + "UserGetUserObjArgs", "UserGetUserObjResult", + "UserGetNextUidArgs", "UserGetNextUidResult", + "UserHasLocalAdministratorSetUpArgs", "UserHasLocalAdministratorSetUpResult", + "UserSetupLocalAdministratorArgs", "UserSetupLocalAdministratorResult", + "UserSetPasswordArgs", "UserSetPasswordResult", + "UserProvisioningUriArgs", "UserProvisioningUriResult", + "UserTwofactorConfigArgs", "UserTwofactorConfigResult", + "UserVerifyTwofactorTokenArgs", "UserVerifyTwofactorTokenResult", + "UserUnset2faSecretArgs", "UserUnset2faSecretResult", "UserRenew2faSecretArgs", "UserRenew2faSecretResult"] @@ -89,6 +105,157 @@ class UserUpdateResult(BaseModel): result: int +class UserDeleteOptions(BaseModel): + delete_group: bool = True + "Deletes the user primary group if it is not being used by any other user." + + +class UserDeleteArgs(BaseModel): + id: int + options: UserDeleteOptions = Field(default=UserDeleteOptions()) + + +class UserDeleteResult(BaseModel): + result: int + + +class UserShellChoicesArgs(BaseModel): + group_ids: list[int] = [] + + +class UserShellChoicesResult(BaseModel): + result: dict = Field(examples=[ + { + '/usr/bin/bash': 'bash', + '/usr/bin/rbash': 'rbash', + '/usr/bin/dash': 'dash', + '/usr/bin/sh': 'sh', + '/usr/bin/zsh': 'zsh', + '/usr/bin/tmux': 'tmux', + '/usr/sbin/nologin': 'nologin' + }, + ]) + + +@single_argument_args("get_user_obj") +class UserGetUserObjArgs(BaseModel): + username: str | None = None + uid: int | None = None + get_groups: bool = False + "retrieve group list for the specified user." + sid_info: bool = False + "retrieve SID and domain information for the user." + + +@single_argument_result +class UserGetUserObjResult(BaseModel): + pw_name: str + "name of the user" + pw_gecos: str + "full username or comment field" + pw_dir: str + "user home directory" + pw_shell: str + "user command line interpreter" + pw_uid: int + "numerical user id of the user" + pw_gid: int + "numerical group id for the user's primary group" + grouplist: list[int] | None + """ + optional list of group ids for groups of which this account is a member. If `get_groups` is not specified, + this value will be null. + """ + sid: str | None + "optional SID value for the account that is present if `sid_info` is specified in payload." + source: Literal['LOCAL', 'ACTIVEDIRECTORY', 'LDAP'] + "the source for the user account." + local: bool + "boolean value indicating whether the account is local to TrueNAS or provided by a directory service." + + +class UserGetNextUidArgs(BaseModel): + pass + + +class UserGetNextUidResult(BaseModel): + result: int + + +class UserHasLocalAdministratorSetUpArgs(BaseModel): + pass + + +class UserHasLocalAdministratorSetUpResult(BaseModel): + result: bool + + +class UserSetupLocalAdministratorEC2Options(BaseModel): + instance_id: NonEmptyString + + +class UserSetupLocalAdministratorOptions(BaseModel): + ec2: UserSetupLocalAdministratorEC2Options | None = None + + +class UserSetupLocalAdministratorArgs(BaseModel): + username: Literal['root', 'truenas_admin'] + password: Private[str] + options: UserSetupLocalAdministratorOptions = Field(default=UserSetupLocalAdministratorOptions()) + + +class UserSetupLocalAdministratorResult(BaseModel): + result: None + + +@single_argument_args("set_password_data") +class UserSetPasswordArgs(BaseModel): + username: str + old_password: Private[str | None] = None + new_password: Private[NonEmptyString] + + +class UserSetPasswordResult(BaseModel): + result: None + + +class UserProvisioningUriArgs(BaseModel): + username: str + + +class UserProvisioningUriResult(BaseModel): + result: str + + +class UserTwofactorConfigArgs(BaseModel): + username: str + + +@single_argument_result +class UserTwofactorConfigResult(BaseModel): + provisioning_uri: str | None + secret_configured: bool + interval: int + otp_digits: int + + +class UserVerifyTwofactorTokenArgs(BaseModel): + username: str + token: Private[str | None] = None + + +class UserVerifyTwofactorTokenResult(BaseModel): + result: bool + + +class UserUnset2faSecretArgs(BaseModel): + username: str + + +class UserUnset2faSecretResult(BaseModel): + result: None + + class TwofactorOptions(BaseModel, metaclass=ForUpdateMetaclass): otp_digits: Annotated[int, Ge(6), Le(8)] "Represents number of allowed digits in the OTP" diff --git a/src/middlewared/middlewared/plugins/account.py b/src/middlewared/middlewared/plugins/account.py index bc5c96738a748..f4b69978ba066 100644 --- a/src/middlewared/middlewared/plugins/account.py +++ b/src/middlewared/middlewared/plugins/account.py @@ -5,14 +5,12 @@ import shlex import shutil import stat -import warnings import wbclient from pathlib import Path from contextlib import suppress from middlewared.api import api_method from middlewared.api.current import * -from middlewared.schema import accepts, Bool, Dict, Int, List, Password, Patch, returns, SID, Str from middlewared.service import ( CallError, CRUDService, ValidationErrors, no_auth_required, no_authz_required, pass_app, private, filterable, job ) @@ -24,7 +22,6 @@ from middlewared.utils.nss import pwd, grp from middlewared.utils.nss.nss_common import NssModule from middlewared.utils.privilege import credential_has_full_admin, privileges_group_mapping -from middlewared.validators import Range from middlewared.async_validators import check_path_resides_within_volume from middlewared.utils.sid import db_id_to_rid, DomainRid from middlewared.plugins.account_.constants import ( @@ -34,7 +31,6 @@ from middlewared.plugins.idmap_.idmap_constants import ( BASE_SYNTHETIC_DATASTORE_ID, IDType, - TRUENAS_IDMAP_DEFAULT_LOW, SID_LOCAL_USER_PREFIX, SID_LOCAL_GROUP_PREFIX ) @@ -856,16 +852,7 @@ def recreate_homedir_if_not_exists(self, user, group, mode): 'options': {'stripacl': True}, }).wait_sync(raise_error=True) - @accepts( - Int('id'), - Dict( - 'options', - Bool('delete_group', default=True), - ), - audit='Delete user', - audit_callback=True, - ) - @returns(Int('primary_key')) + @api_method(UserDeleteArgs, UserDeleteResult, audit='Delete user', audit_callback=True) def do_delete(self, audit_callback, pk, options): """ Delete user `id`. @@ -951,20 +938,7 @@ def do_delete(self, audit_callback, pk, options): return pk - @accepts(List('group_ids', items=[Int('group_id')])) - @returns(Dict( - 'shell_info', - Str('shell_path'), - example={ - '/usr/bin/bash': 'bash', - '/usr/bin/rbash': 'rbash', - '/usr/bin/dash': 'dash', - '/usr/bin/sh': 'sh', - '/usr/bin/zsh': 'zsh', - '/usr/bin/tmux': 'tmux', - '/usr/sbin/nologin': 'nologin' - } - )) + @api_method(UserShellChoicesArgs, UserShellChoicesResult) def shell_choices(self, group_ids): """ Return the available shell choices to be used in `user.create` and `user.update`. @@ -998,62 +972,13 @@ def shell_choices(self, group_ids): return shells - @accepts(Dict( - 'get_user_obj', - Str('username', default=None), - Int('uid', default=None), - Bool('get_groups', default=False), - Bool('sid_info', default=False), - ), roles=['ACCOUNT_READ']) - @returns(Dict( - 'user_information', - Str('pw_name'), - Str('pw_gecos'), - Str('pw_dir'), - Str('pw_shell'), - Int('pw_uid'), - Int('pw_gid'), - List('grouplist'), - SID('sid', null=True), - Str('source', enum=['LOCAL', 'ACTIVEDIRECTORY', 'LDAP']), - Bool('local'), - register=True, - )) + @api_method(UserGetUserObjArgs, UserGetUserObjResult, roles=['ACCOUNT_READ']) def get_user_obj(self, data): """ Returns dictionary containing information from struct passwd for the user specified by either the username or uid. Bypasses user cache. - Supports the following optional parameters: - `get_groups` - retrieve group list for the specified user. - - NOTE: results will not include nested groups for Active Directory users - - `sid_info` - retrieve SID and domain information for the user - - Returns object with following keys: - - `pw_name` - name of the user - - `pw_uid` - numerical user id of the user - - `pw_gid` - numerical group id for the user's primary group - - `pw_gecos` - full username or comment field - - `pw_dir` - user home directory - - `pw_shell` - user command line interpreter - - `local` - boolean value indicating whether the account is local to TrueNAS or provided by - a directory service. - - `grouplist` - optional list of group ids for groups of which this account is a member. If `get_groups` - is not specified, this value will be null. - - `sid` - optional SID value for the account that is present if `sid_info` is specified in payload. - - `source` - the source for the user account. + NOTE: results will not include nested groups for Active Directory users. """ verrors = ValidationErrors() if not data['username'] and data['uid'] is None: @@ -1095,6 +1020,8 @@ def get_user_obj(self, data): if data['get_groups']: user_obj['grouplist'] = os.getgrouplist(user_obj['pw_name'], user_obj['pw_gid']) + else: + user_obj['grouplist'] = None if data['sid_info']: match user_obj['source']: @@ -1143,8 +1070,7 @@ def get_user_obj(self, data): return user_obj - @accepts(roles=['ACCOUNT_READ']) - @returns(Int('next_available_uid')) + @api_method(UserGetNextUidArgs, UserGetNextUidResult, roles=['ACCOUNT_READ']) async def get_next_uid(self): """ Get the next available/free uid. @@ -1167,19 +1093,7 @@ async def get_next_uid(self): return last_uid + 1 @no_auth_required - @accepts() - @returns(Bool()) - async def has_root_password(self): - """ - Deprecated method. Use `user.has_local_administrator_set_up` - """ - warnings.warn("`user.has_root_password` has been deprecated. Use `user.has_local_administrator_set_up`", - DeprecationWarning) - return await self.has_local_administrator_set_up() - - @no_auth_required - @accepts() - @returns(Bool()) + @api_method(UserHasLocalAdministratorSetUpArgs, UserHasLocalAdministratorSetUpResult) async def has_local_administrator_set_up(self): """ Return whether a local administrator with a valid password exists. @@ -1190,20 +1104,7 @@ async def has_local_administrator_set_up(self): return len(await self.middleware.call('privilege.local_administrators')) > 0 @no_auth_required - @accepts( - Str('username', enum=['root', 'truenas_admin']), - Password('password'), - Dict( - 'options', - Dict( - 'ec2', - Str('instance_id', required=True), - ), - update=True, - ), - audit='Set up local administrator', - ) - @returns() + @api_method(UserSetupLocalAdministratorArgs, UserSetupLocalAdministratorResult, audit='Set up local administrator') @pass_app() async def setup_local_administrator(self, app, username, password, options): """ @@ -1544,12 +1445,8 @@ def update_sshpubkey(self, homedir, user, group): f.write(f'{pubkey}\n') @no_authz_required - @accepts(Dict( - 'set_password_data', - Str('username', required=True), - Password('old_password', default=None), - Password('new_password', required=True), - ), audit='Set account password', audit_extended=lambda data: data['username']) + @api_method(UserSetPasswordArgs, UserSetPasswordResult, + audit='Set account password', audit_extended=lambda data: data['username']) @pass_app(require=True) async def set_password(self, app, data): """ @@ -1681,19 +1578,7 @@ class Config: datastore_extend_context = 'group.group_extend_context' cli_namespace = 'account.group' role_prefix = 'ACCOUNT' - - ENTRY = Patch( - 'group_create', 'group_entry', - ('rm', {'name': 'allow_duplicate_gid'}), - ('add', Int('id')), - ('add', Str('group')), - ('add', Bool('builtin')), - ('add', Bool('id_type_both')), - ('add', Bool('local')), - ('add', Str('sid', null=True)), - ('add', List('roles', items=[Str('role')])), - register=True - ) + entry = GroupEntry @private async def group_extend_context(self, rows, extra): @@ -1801,29 +1686,10 @@ async def query(self, filters, options): filter_list, result + ds_groups, filters, options ) - @accepts(Dict( - 'group_create', - Int('gid', validators=[Range(0, TRUENAS_IDMAP_DEFAULT_LOW - 1)]), - Str('name', required=True), - Bool('smb', default=True), - List('sudo_commands', items=[Str('command', empty=False)]), - List('sudo_commands_nopasswd', items=[Str('command', empty=False)]), - Bool('allow_duplicate_gid', default=False), - List('users', items=[Int('id')], required=False), - register=True, - ), audit='Create group', audit_extended=lambda data: data['name']) - @returns(Int('primary_key')) + @api_method(GroupCreateArgs, GroupCreateResult, audit='Create group', audit_extended=lambda data: data['name']) async def do_create(self, data): """ Create a new group. - - If `gid` is not provided it is automatically filled with the next one available. - - `allow_duplicate_gid` allows distinct group names to share the same gid. - - `users` is a list of user ids (`id` attribute from `user.query`). - - `smb` specifies whether the group should be mapped into an NT group. """ return await self.create_internal(data) @@ -1858,17 +1724,7 @@ async def create_internal(self, data, reload_users=True): return pk - @accepts( - Int('id'), - Patch( - 'group_create', - 'group_update', - ('attr', {'update': True}), - ), - audit='Update group', - audit_callback=True, - ) - @returns(Int('primary_key')) + @api_method(GroupUpdateArgs, GroupUpdateResult, audit='Update group', audit_callback=True) async def do_update(self, audit_callback, pk, data): """ Update attributes of an existing group. @@ -1954,8 +1810,7 @@ async def do_update(self, audit_callback, pk, data): await self.middleware.call('service.reload', 'user') return pk - @accepts(Int('id'), Dict('options', Bool('delete_users', default=False)), audit='Delete group', audit_callback=True) - @returns(Int('primary_key')) + @api_method(GroupDeleteArgs, GroupDeleteResult, audit='Delete group', audit_callback=True) async def do_delete(self, audit_callback, pk, options): """ Delete group `id`. @@ -2015,8 +1870,7 @@ async def do_delete(self, audit_callback, pk, options): return pk - @accepts(roles=['ACCOUNT_READ']) - @returns(Int('next_available_gid')) + @api_method(GroupGetNextGidArgs, GroupGetNextGidResult, roles=['ACCOUNT_READ']) async def get_next_gid(self): """ Get the next available/free gid. @@ -2034,21 +1888,7 @@ async def get_next_gid(self): return next_gid - @accepts(Dict( - 'get_group_obj', - Str('groupname', default=None), - Int('gid', default=None), - Bool('sid_info', default=False) - ), roles=['ACCOUNT_READ']) - @returns(Dict( - 'group_info', - Str('gr_name'), - Int('gr_gid'), - List('gr_mem'), - SID('sid', null=True), - Str('source', enum=['LOCAL', 'ACTIVEDIRECTORY', 'LDAP']), - Bool('local'), - )) + @api_method(GroupGetGroupObjArgs, GroupGetGroupObjResult, roles=['ACCOUNT_READ']) def get_group_obj(self, data): """ Returns dictionary containing information from struct grp for the group specified by either @@ -2056,23 +1896,6 @@ def get_group_obj(self, data): If `sid_info` is specified then addition SMB / domain information is returned for the group. - - Output contains following keys: - - `gr_name` - name of the group - - `gr_gid` - group id of the group - - `gr_mem` - list of gids that are members of the group - - `local` - boolean indicating whether this group is local to the NAS or provided by a - directory service. - - `sid` - optional SID value for the account that is present if `sid_info` is specified in payload. - - `source` - the name server switch module that provided the user. Options are: - FILES - local user in passwd file of server, WINBIND - user provided by winbindd, SSS - user - provided by SSSD. """ verrors = ValidationErrors() if not data['groupname'] and data['gid'] is None: diff --git a/src/middlewared/middlewared/plugins/account_/2fa.py b/src/middlewared/middlewared/plugins/account_/2fa.py index 8c1bea08f8240..efc4b296a3c9e 100644 --- a/src/middlewared/middlewared/plugins/account_/2fa.py +++ b/src/middlewared/middlewared/plugins/account_/2fa.py @@ -3,11 +3,9 @@ from middlewared.api import api_method from middlewared.api.current import * -from middlewared.schema import accepts, Bool, Dict, Int, Password, Ref, returns, Str from middlewared.service import CallError, no_authz_required, pass_app, private, Service from middlewared.plugins.system.product import PRODUCT_NAME from middlewared.utils.privilege import app_credential_full_admin_or_user -from middlewared.validators import Range class UserService(Service): @@ -23,8 +21,7 @@ async def provisioning_uri_internal(self, username, user_twofactor_config): 'iXsystems' ) - @accepts(Str('username')) - @returns(Str(title='Provisioning URI')) + @api_method(UserProvisioningUriArgs, UserProvisioningUriResult) async def provisioning_uri(self, username): """ Returns the provisioning URI for the OTP for `username`. This can then be encoded in a QR code and used @@ -39,14 +36,7 @@ async def provisioning_uri(self, username): return await self.provisioning_uri_internal(username, user_twofactor_config) - @accepts(Str('username')) - @returns(Dict( - 'user_twofactor_config', - Str('provisioning_uri', null=True), - Bool('secret_configured'), - Int('interval', validators=[Range(min_=5)]), - Int('otp_digits', validators=[Range(min_=6, max_=8)]), - )) + @api_method(UserTwofactorConfigArgs, UserTwofactorConfigResult) async def twofactor_config(self, username): """ Returns two-factor authentication configuration settings for specified `username`. @@ -67,8 +57,7 @@ async def twofactor_config(self, username): 'otp_digits': user_twofactor_config['otp_digits'], } - @accepts(Str('username'), Password('token', null=True)) - @returns(Bool('token_verified')) + @api_method(UserVerifyTwofactorTokenArgs, UserVerifyTwofactorTokenResult) def verify_twofactor_token(self, username, token): """ Returns boolean true if provided `token` is successfully authenticated for `username`. @@ -102,8 +91,8 @@ async def translate_username(self, username): return await self.middleware.call('user.query', [['username', '=', user['pw_name']]], {'get': True}) - @accepts(Str('username'), audit='Unset two-factor authentication secret:', audit_extended=lambda username: username) - @returns() + @api_method(UserUnset2faSecretArgs, UserUnset2faSecretResult, + audit='Unset two-factor authentication secret:', audit_extended=lambda username: username) async def unset_2fa_secret(self, username): """ Unset two-factor authentication secret for `username`. diff --git a/src/middlewared/middlewared/plugins/account_/builtin_administrator.py b/src/middlewared/middlewared/plugins/account_/builtin_administrator.py index 5d5742e8b6c33..01b82f1463e32 100644 --- a/src/middlewared/middlewared/plugins/account_/builtin_administrator.py +++ b/src/middlewared/middlewared/plugins/account_/builtin_administrator.py @@ -1,14 +1,12 @@ +from middlewared.api import api_method +from middlewared.api.current import GroupHasPasswordEnabledUserArgs, GroupHasPasswordEnabledUserResult from middlewared.plugins.account import unixhash_is_valid -from middlewared.schema import accepts, Int, List from middlewared.service import filter_list, Service, private class GroupService(Service): - @accepts( - List("gids", items=[Int("gid")]), - List("exclude_user_ids", items=[Int("exclude_user_id")]), - ) + @api_method(GroupHasPasswordEnabledUserArgs, GroupHasPasswordEnabledUserResult) async def has_password_enabled_user(self, gids, exclude_user_ids): """ Checks whether at least one local user with a password is a member of any of the `group_ids`. diff --git a/src/middlewared/middlewared/plugins/account_/privilege.py b/src/middlewared/middlewared/plugins/account_/privilege.py index 92bf0fa9ec14b..5cde64293b5e1 100644 --- a/src/middlewared/middlewared/plugins/account_/privilege.py +++ b/src/middlewared/middlewared/plugins/account_/privilege.py @@ -2,8 +2,9 @@ import errno import wbclient +from middlewared.api import api_method +from middlewared.api.current import * from middlewared.plugins.account import unixhash_is_valid -from middlewared.schema import accepts, Bool, Dict, Int, List, Ref, SID, Str, Patch from middlewared.service import CallError, CRUDService, filter_list, private, ValidationErrors from middlewared.service_exception import MatchNotFound from middlewared.utils.privilege import ( @@ -43,22 +44,7 @@ class Config: datastore_extend = "privilege.item_extend" datastore_extend_context = "privilege.item_extend_context" cli_namespace = "auth.privilege" - - ENTRY = Dict( - "privilege_entry", - Int("id"), - Str("builtin_name", null=True), - Str("name", required=True, empty=False), - List("local_groups", items=[Ref("group_entry")]), - List("ds_groups", items=[Ref("group_entry")]), - List("allowlist", items=[Dict( - "allowlist_item", - Str("method", required=True, enum=["GET", "POST", "PUT", "DELETE", "CALL", "SUBSCRIBE", "*"]), - Str("resource", required=True), - )]), - List("roles", items=[Str("role")]), - Bool("web_shell", required=True), - ) + entry = PrivilegeEntry @private async def item_extend_context(self, rows, extra): @@ -72,16 +58,8 @@ async def item_extend(self, item, context): item["ds_groups"] = await self._ds_groups(context["groups"], item["ds_groups"]) return item - @accepts(Patch( - "privilege_entry", - "privilege_create", - ("rm", {"name": "builtin_name"}), - ("rm", {"name": "local_groups"}), - ("rm", {"name": "ds_groups"}), - ("add", List("local_groups", items=[Int("local_group")])), - ("add", List("ds_groups", items=[Int("ds_group_gid"), SID("ds_group_sid")])), - register=True - ), audit="Create privilege", audit_extended=lambda data: data["name"]) + @api_method(PrivilegeCreateArgs, PrivilegeCreateResult, + audit="Create privilege", audit_extended=lambda data: data["name"]) async def do_create(self, data): """ Creates a privilege. @@ -106,16 +84,7 @@ async def do_create(self, data): return await self.get_instance(id_) - @accepts( - Int("id", required=True), - Patch( - "privilege_create", - "privilege_update", - ("attr", {"update": True}), - ), - audit="Update privilege", - audit_callback=True, - ) + @api_method(PrivilegeUpdateArgs, PrivilegeUpdateResult, audit="Update privilege", audit_callback=True) async def do_update(self, audit_callback, id_, data): """ Update the privilege `id`. @@ -179,11 +148,7 @@ async def do_update(self, audit_callback, id_, data): return await self.get_instance(id_) - @accepts( - Int("id"), - audit="Delete privilege", - audit_callback=True, - ) + @api_method(PrivilegeDeleteArgs, PrivilegeDeleteResult, audit="Delete privilege", audit_callback=True) async def do_delete(self, audit_callback, id_): """ Delete the privilege `id`. diff --git a/src/middlewared/middlewared/plugins/auth.py b/src/middlewared/middlewared/plugins/auth.py index 717541f5913ec..95a41556c883d 100644 --- a/src/middlewared/middlewared/plugins/auth.py +++ b/src/middlewared/middlewared/plugins/auth.py @@ -5,7 +5,9 @@ import time import warnings +from middlewared.api import api_method from middlewared.api.base.server.ws_handler.rpc import RpcWebSocketAppEvent +from middlewared.api.current import AuthMeArgs, AuthMeResult from middlewared.auth import (UserSessionManagerCredentials, UnixSocketSessionManagerCredentials, LoginPasswordSessionManagerCredentials, ApiKeySessionManagerCredentials, TrueNasNodeSessionManagerCredentials, TokenSessionManagerCredentials, @@ -518,15 +520,7 @@ async def logout(self, app): return True @no_authz_required - @accepts() - @returns( - Patch( - 'user_information', - 'current_user_information', - ('add', Dict('attributes', additional_attrs=True)), - ('add', Dict('two_factor_config', additional_attrs=True)), - ) - ) + @api_method(AuthMeArgs, AuthMeResult) @pass_app() async def me(self, app): """