diff --git a/src/middlewared/middlewared/api/v25_04_0/__init__.py b/src/middlewared/middlewared/api/v25_04_0/__init__.py index 64711ddf91dd..fd3e5097fef0 100644 --- a/src/middlewared/middlewared/api/v25_04_0/__init__.py +++ b/src/middlewared/middlewared/api/v25_04_0/__init__.py @@ -32,6 +32,7 @@ from .rdma import * # noqa from .rdma_interface import * # noqa from .smartctl import * # noqa +from .smb import * # noqa from .snmp import * # noqa from .static_route import * # noqa from .system_lifecycle import * # noqa diff --git a/src/middlewared/middlewared/api/v25_04_0/smb.py b/src/middlewared/middlewared/api/v25_04_0/smb.py new file mode 100644 index 000000000000..e5e82eadff03 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/smb.py @@ -0,0 +1,65 @@ +from middlewared.api.base import ( + BaseModel, + NonEmptyString, + single_argument_args, + SID, +) +from pydantic import Field, model_validator +from typing import Literal, Self + +__all__ = [ + 'GetSmbAclArgs', 'GetSmbAclResult', + 'SetSmbAclArgs', 'SetSmbAclResult', +] + + +class SMBShareAclEntryWhoId(BaseModel): + id_type: Literal['USER', 'GROUP', 'BOTH'] + xid: int = Field(alias='id') + + +class SMBShareAclEntry(BaseModel): + ae_perm: Literal['FULL', 'CHANGE', 'READ'] + """ Permissions granted to the principal. """ + ae_type: Literal['ALLOWED', 'DENIED'] + """ The type of SMB share ACL entry. """ + ae_who_sid: SID | None = None + """ SID value of principle for whom ACL entry applies. """ + ae_who_id: SMBShareAclEntryWhoId | None = None + """ Unix ID of principle for whom ACL entry applies. """ + ae_who_str: NonEmptyString | None = None + + @model_validator(mode='after') + def check_ae_who(self) -> Self: + if self.ae_who_sid is None and self.ae_who_id is None and self.ae_who_str is None: + raise ValueError( + 'Either ae_who_sid or ae_who_id or ae_who_str is required to identify user or group ' + 'to which the ACL entry applies.' + ) + + return self + + +class SMBShareAcl(BaseModel): + share_name: NonEmptyString + """ Name of the SMB share. """ + share_acl: list[SMBShareAclEntry] = [SMBShareAclEntry(ae_who_sid='S-1-1-0', ae_perm='FULL', ae_type='ALLOWED')] + """ List of SMB share ACL entries """ + + +@single_argument_args('smb_setacl') +class SetSmbAclArgs(SMBShareAcl): + pass + + +class SetSmbAclResult(BaseModel): + result: SMBShareAcl + + +@single_argument_args('smb_getacl') +class GetSmbAclArgs(BaseModel): + share_name: NonEmptyString + + +class GetSmbAclResult(SetSmbAclResult): + pass diff --git a/src/middlewared/middlewared/plugins/smb.py b/src/middlewared/middlewared/plugins/smb.py index b1888bd18ad0..7d6649c83994 100644 --- a/src/middlewared/middlewared/plugins/smb.py +++ b/src/middlewared/middlewared/plugins/smb.py @@ -10,9 +10,14 @@ from copy import deepcopy +from middlewared.api import api_method +from middlewared.api.current import ( + GetSmbAclArgs, GetSmbAclResult, + SetSmbAclArgs, SetSmbAclResult, +) from middlewared.common.attachment import LockableFSAttachmentDelegate from middlewared.common.listen import SystemServiceListenMultipleDelegate -from middlewared.schema import Bool, Dict, IPAddr, List, NetbiosName, NetbiosDomain, Ref, returns, SID, Str, Int, Patch +from middlewared.schema import Bool, Dict, IPAddr, List, NetbiosName, NetbiosDomain, Str, Int, Patch from middlewared.schema import Path as SchemaPath # List schema defaults to [], supplying NOT_PROVIDED avoids having audit update that # defaults for ignore_list or watch_list from overrwriting previous value @@ -1529,25 +1534,12 @@ async def presets(self): """ return {x.name: x.value for x in SMBSharePreset} - @accepts(Dict( - 'smb_share_acl', - Str('share_name', required=True), - List('share_acl', items=[ - Dict( - 'aclentry', - SID('ae_who_sid', default=None), - Dict( - 'ae_who_id', - Str('id_type', enum=['USER', 'GROUP', 'BOTH']), - Int('id') - ), - Str('ae_perm', enum=['FULL', 'CHANGE', 'READ'], required=True), - Str('ae_type', enum=['ALLOWED', 'DENIED'], required=True) - ), - ], default=[{'ae_who_sid': 'S-1-1-0', 'ae_perm': 'FULL', 'ae_type': 'ALLOWED'}]), - register=True - ), roles=['SHARING_SMB_WRITE'], audit='Setacl SMB share', audit_extended=lambda data: data['share_name']) - @returns(Ref('smb_share_acl')) + @api_method( + SetSmbAclArgs, SetSmbAclResult, + roles=['SHARING_SMB_WRITE'], + audit='Setacl SMB share', + audit_extended=lambda data: data['share_name'] + ) async def setacl(self, data): """ Set an ACL on `share_name`. This only impacts access through the SMB protocol. @@ -1657,11 +1649,12 @@ async def setacl(self, data): }) return await self.getacl({'share_name': data['share_name']}) - @accepts(Dict( - 'smb_getacl', - Str('share_name', required=True) - ), roles=['SHARING_SMB_READ'], audit='Getacl SMB share', audit_extended=lambda data: data['share_name']) - @returns(Ref('smb_share_acl')) + @api_method( + GetSmbAclArgs, GetSmbAclResult, + roles=['SHARING_SMB_READ'], + audit='Getacl SMB share', + audit_extended=lambda data: data['share_name'] + ) async def getacl(self, data): verrors = ValidationErrors()