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-132041 / 25.04 / Allow setting ACL entries by user or group name #13965

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions src/middlewared/middlewared/api/v25_04_0/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .acme_protocol import * # noqa
from .acl import * # noqa
from .alert import * # noqa
from .alertservice import * # noqa
from .api_key import * # noqa
Expand Down
372 changes: 372 additions & 0 deletions src/middlewared/middlewared/api/v25_04_0/acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
from annotated_types import Ge, Le
from middlewared.api.base import (
BaseModel,
Excluded,
excluded_field,
ForUpdateMetaclass,
LocalUsername,
NonEmptyString,
RemoteUsername,
single_argument_args,
)
from pydantic import Field, model_validator
from typing import Annotated, Literal, Self
from middlewared.utils.filesystem.acl import (
ACL_UNDEFINED_ID,
FS_ACL_Type,
NFS4ACE_Tag,
NFS4ACE_Type,
NFS4ACE_MaskSimple,
NFS4ACE_FlagSimple,
POSIXACE_Tag,
NFS4_SPECIAL_ENTRIES,
POSIX_SPECIAL_ENTRIES,
)
from .common import QueryFilters, QueryOptions

__all__ = [
'AclTemplateEntry',
'AclTemplateByPathArgs', 'AclTemplateByPathResult',
'AclTemplateCreateArgs', 'AclTemplateCreateResult',
'AclTemplateUpdateArgs', 'AclTemplateUpdateResult',
'AclTemplateDeleteArgs', 'AclTemplateDeleteResult',
'FilesystemAddToAclArgs', 'FilesystemAddToAclResult',
'FilesystemGetAclArgs', 'FilesystemGetAclResult',
'FilesystemSetAclArgs', 'FilesystemSetAclResult',
'FilesystemGetInheritedAclArgs', 'FilesystemGetInheritedAclResult'
]

ACL_MAX_ID = 2 ** 32 // 2 - 1

AceWhoId = Annotated[int, Ge(ACL_UNDEFINED_ID), Le(ACL_MAX_ID)]

NFS4ACE_BasicPermset = Literal[
NFS4ACE_MaskSimple.FULL_CONTROL,
NFS4ACE_MaskSimple.MODIFY,
NFS4ACE_MaskSimple.READ,
NFS4ACE_MaskSimple.TRAVERSE
]

NFS4ACE_BasicFlagset = Literal[
NFS4ACE_FlagSimple.INHERIT,
NFS4ACE_FlagSimple.NOINHERIT,
]

NFS4ACE_Tags = Literal[
NFS4ACE_Tag.SPECIAL_OWNER,
NFS4ACE_Tag.SPECIAL_GROUP,
NFS4ACE_Tag.SPECIAL_EVERYONE,
NFS4ACE_Tag.USER,
NFS4ACE_Tag.GROUP
]

NFS4ACE_EntryTypes = Literal[
NFS4ACE_Type.ALLOW,
NFS4ACE_Type.DENY
]


class NFS4ACE_AdvancedPerms(BaseModel):
READ_DATA: bool = False
WRITE_DATA: bool = False
APPEND_DATA: bool = False
READ_NAMED_ATTRS: bool = False
WRITE_NAMED_ATTRS: bool = False
EXECUTE: bool = False
DELETE: bool = False
DELETE_CHILD: bool = False
READ_ATTRIBUTES: bool = False
WRITE_ATTRIBUTES: bool = False
READ_ACL: bool = False
WRITE_ACL: bool = False
WRITE_OWNER: bool = False
SYNCHRONIZE: bool = False


class NFS4ACE_BasicPerms(BaseModel):
BASIC: NFS4ACE_BasicPermset


class NFS4ACE_AdvancedFlags(BaseModel):
FILE_INHERIT: bool = False
DIRECTORY_INHERIT: bool = False
NO_PROPAGATE_INHERIT: bool = False
INHERIT_ONLY: bool = False
INHERITED: bool = False

@model_validator(mode='after')
def check_inherit_only(self) -> Self:
if not self.INHERIT_ONLY:
return self

if not self.FILE_INHERIT and not self.DIRECTORY_INHERIT:
raise ValueError(
'At least one of FILE_INHERIT or DIRECTORY_INHERIT must '
'be set if INHERIT_ONLY is present in the ACE flags'
)

return self


class NFS4ACE_BasicFlags(BaseModel):
BASIC: NFS4ACE_BasicFlagset


class NFS4ACE(BaseModel):
tag: NFS4ACE_Tags
type: NFS4ACE_EntryTypes
perms: NFS4ACE_AdvancedPerms | NFS4ACE_BasicPerms
flags: NFS4ACE_AdvancedFlags | NFS4ACE_BasicFlags
id: AceWhoId | None = None
who: LocalUsername | RemoteUsername | None = None

@model_validator(mode='after')
def check_ace_valid(self) -> Self:
if self.tag in NFS4_SPECIAL_ENTRIES:
if self.id not in (-1, None):
raise ValueError(
f'{self.id}: id may not be specified for the following ACL entry '
f'tags: {", ".join([tag for tag in NFS4_SPECIAL_ENTRIES])}'
)
else:
if not self.who and self.id in (None, -1):
raise ValueError(
'Numeric ID "id" or account name "who" must be specified'
)

return self


class NFS4ACL_Flags(BaseModel):
autoinherit: bool = False
protected: bool = False
defaulted: bool = False


POSIXACE_Tags = Literal[
POSIXACE_Tag.USER_OBJ,
POSIXACE_Tag.GROUP_OBJ,
POSIXACE_Tag.OTHER,
POSIXACE_Tag.MASK,
POSIXACE_Tag.USER,
POSIXACE_Tag.GROUP
]


class POSIXACE_Perms(BaseModel):
READ: bool
WRITE: bool
EXECUTE: bool


class POSIXACE(BaseModel):
tag: POSIXACE_Tags
perms: POSIXACE_Perms
default: bool
id: AceWhoId | None = None
who: LocalUsername | RemoteUsername | None = None

@model_validator(mode='after')
def check_ace_valid(self) -> Self:
if self.tag in POSIX_SPECIAL_ENTRIES:
if self.id not in (-1, None):
raise ValueError(
f'{self.id}: id may not be specified for the following ACL entry '
f'tags: {", ".join([tag for tag in POSIX_SPECIAL_ENTRIES])}'
)
else:
if not self.who and self.id in (None, -1):
raise ValueError(
'Numeric ID "id" or account name "who" must be specified'
)

return self


class AclBaseInfo(BaseModel):
uid: AceWhoId | None
gid: AceWhoId | None


class NFS4ACL(AclBaseInfo):
acltype: Literal[FS_ACL_Type.NFS4]
acl: list[NFS4ACE]
aclflags: NFS4ACL_Flags
trivial: bool


class POSIXACL(AclBaseInfo):
acltype: Literal[FS_ACL_Type.POSIX1E]
acl: list[POSIXACE]
trivial: bool


class DISABLED_ACL(AclBaseInfo):
# ACL response paths with ACL entirely disabled
acltype: Literal[FS_ACL_Type.DISABLED]
acl: Literal[None]
trivial: Literal[True]


class FilesystemGetAclArgs(BaseModel):
path: NonEmptyString
simplified: bool = True
resolve_ids: bool = False


class AclBaseResult(BaseModel):
path: NonEmptyString
user: NonEmptyString | None
group: NonEmptyString | None


class NFS4ACLResult(NFS4ACL, AclBaseResult):
pass


class POSIXACLResult(POSIXACL, AclBaseResult):
pass


class DISABLED_ACLResult(DISABLED_ACL, AclBaseResult):
pass


class FilesystemGetAclResult(BaseModel):
result: NFS4ACLResult | POSIXACLResult | DISABLED_ACLResult


class FilesystemSetAclOptions(BaseModel):
stripacl: bool = False
recursive: bool = False
traverse: bool = False
canonicalize: bool = True
validate_effective_acl: bool = True


@single_argument_args('filesystem_acl')
class FilesystemSetAclArgs(BaseModel):
path: NonEmptyString
dacl: list[NFS4ACE] | list[POSIXACE]
options: FilesystemSetAclOptions = Field(default=FilesystemSetAclOptions())
nfs41_flags: NFS4ACL_Flags = Field(default=NFS4ACL_Flags())
uid: AceWhoId | None = ACL_UNDEFINED_ID
user: str | None = None
gid: AceWhoId | None = ACL_UNDEFINED_ID
group: str | None = None

# acltype is explicitly added to preserve compatibility with older setacl API
acltype: Literal[FS_ACL_Type.NFS4, FS_ACL_Type.POSIX1E] | None = None

@model_validator(mode='after')
def check_setacl_valid(self) -> Self:
if len(self.dacl) != 0 and self.options.stripacl:
raise ValueError(
'Simultaneosuly setting and removing ACL from path is not supported'
)

return self


class FilesystemSetAclResult(FilesystemGetAclResult):
pass


class AclTemplateEntry(BaseModel):
id: int
builtin: bool
name: str
acltype: Literal[FS_ACL_Type.NFS4, FS_ACL_Type.POSIX1E]
acl: list[NFS4ACE] | list[POSIXACE]
comment: str = ''


class AclTemplateCreate(AclTemplateEntry):
id: Excluded = excluded_field()
builtin: Excluded = excluded_field()


class AclTemplateCreateArgs(BaseModel):
acltemplate_create: AclTemplateCreate


class AclTemplateCreateResult(BaseModel):
result: AclTemplateEntry


class AclTemplateUpdate(AclTemplateCreate, metaclass=ForUpdateMetaclass):
pass


class AclTemplateUpdateArgs(BaseModel):
id: int
acltemplate_update: AclTemplateUpdate


class AclTemplateUpdateResult(BaseModel):
result: AclTemplateEntry


class AclTemplateDeleteArgs(BaseModel):
id: int


class AclTemplateDeleteResult(BaseModel):
result: int


class AclTemplateFormatOptions(BaseModel):
canonicalize: bool = False
ensure_builtins: bool = False
resolve_names: bool = False


@single_argument_args('filesystem_acl')
class AclTemplateByPathArgs(BaseModel):
path: str = ""
query_filters: QueryFilters = Field(alias='query-filters', default=[])
query_options: QueryOptions = Field(alias='query-options', default=QueryOptions())
format_options: AclTemplateFormatOptions = Field(alias='format-options', default=AclTemplateFormatOptions())


class AclTemplateByPathResult(BaseModel):
result: list[AclTemplateEntry]


class SimplifiedAclEntry(BaseModel):
id_type: Literal[NFS4ACE_Tag.USER, NFS4ACE_Tag.GROUP]
id: int
access: Literal[
NFS4ACE_MaskSimple.READ,
NFS4ACE_MaskSimple.MODIFY,
NFS4ACE_MaskSimple.FULL_CONTROL
]


class FilesystemAddToAclOptions(BaseModel):
force: bool = False


@single_argument_args('add_to_acl')
class FilesystemAddToAclArgs(BaseModel):
path: NonEmptyString
entries: list[SimplifiedAclEntry]
options: FilesystemAddToAclOptions = Field(default=FilesystemAddToAclOptions())


class FilesystemAddToAclResult(BaseModel):
result: bool


class FSGetInheritedAclOptions(BaseModel):
directory: bool = True


@single_argument_args('calculate_inherited_acl')
class FilesystemGetInheritedAclArgs(BaseModel):
path: NonEmptyString
options: FSGetInheritedAclOptions = Field(default=FSGetInheritedAclOptions())


class FilesystemGetInheritedAclResult(BaseModel):
result: list[NFS4ACE] | list[POSIXACE]
Loading
Loading