Skip to content

Commit

Permalink
Add ability to set ACL entries by username (#13965)
Browse files Browse the repository at this point in the history
This commit converts filesystem ACL APIs to use new api_method
as well as improving the filesystem.setacl API to allow setting
ACL entries by name. This commit also removes several private
APIs.
  • Loading branch information
anodos325 authored Oct 30, 2024
1 parent 6b667c4 commit d869cd2
Show file tree
Hide file tree
Showing 11 changed files with 1,397 additions and 774 deletions.
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

0 comments on commit d869cd2

Please sign in to comment.