# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-## Copyright 2022 Canonical Ltd.## This program is free software; you can redistribute it and/or# modify it under the terms of the GNU Lesser General Public# License version 3 as published by the Free Software Foundation.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU# Lesser General Public License for more details.## You should have received a copy of the GNU Lesser General Public License# along with this program. If not, see <http://www.gnu.org/licenses/>."""Specify and apply permissions and ownership to part-owned files."""importosfromfnmatchimportfnmatchfrompathlibimportPathfromtypingimportAnyfrompydanticimportBaseModel,model_validator
[docs]classPermissions(BaseModel):"""Description of the ownership and permission settings for a set of files. A ``Permissions`` object specifies that a given pattern-like ``path`` should be owned by ``owner`` with a given ``group``, and have the read/write/execute bits defined by ``mode``. Notes ----- - ``path`` is optional and defaults to "everything"; - ``owner`` and ``group`` are optional if both are omitted - that is, if one of the pair is specified then both must be; - ``mode`` is a string containing an integer in base 8. For example, "755", "0755" and "0o755" are all accepted and are the equivalent of calling ``chmod 755 ...``. """path:str="*"owner:int|None=Nonegroup:int|None=Nonemode:str|None=None# pylint: disable=no-self-argument
[docs]@model_validator(mode="before")@classmethoddefvalidate_root(cls,values:dict[Any,Any])->dict[Any,Any]:"""Validate that "owner" and "group" are correctly specified."""has_owner="owner"invalueshas_group="group"invaluesifhas_owner!=has_group:raiseValueError('If either "owner" or "group" is defined, both must be set')returnvalues
# pylint: enable=no-self-argument@propertydefmode_octal(self)->int:"""Get the mode as a base-8 integer."""ifself.modeisNone:raiseTypeError("'mode' is not set!")returnint(self.mode,base=8)
[docs]defapplies_to(self,path:Path|str)->bool:"""Whether this Permissions' path pattern applies to ``path``."""ifself.path=="*":returnTruereturnfnmatch(str(path),self.path)
[docs]defapply_permissions(self,target:Path|str)->None:"""Apply the permissions configuration to ``target``. Note that this method doesn't check if this ``Permissions``'s path pattern matches ``target``; be sure to call ``applies_to()`` beforehand. """ifself.modeisnotNone:os.chmod(target,self.mode_octal)ifself.ownerisnotNoneandself.groupisnotNone:os.chown(target,self.owner,self.group)
[docs]deffilter_permissions(target:Path|str,permissions:list[Permissions])->list[Permissions]:"""Get the subset of ``permissions`` whose path patterns apply to ``target``."""return[pforpinpermissionsifp.applies_to(target)]
[docs]defapply_permissions(target:Path|str,permissions:list[Permissions])->None:"""Apply all permissions configurations in ``permissions`` to ``target``."""forpermissioninpermissions:permission.apply_permissions(target)
[docs]defpermissions_are_compatible(left:list[Permissions]|None,right:list[Permissions]|None)->bool:"""Whether two sets of permissions definitions are not in conflict with each other. The function determines whether applying the two lists of Permissions to a given path would result in the same ``owner``, ``group`` and ``mode``. Remarks: -------- - If either of the parameters is None or empty, they are considered compatible because they are understood to not be "in conflict". - Otherwise, the permissions are incompatible if one would they would set one of the attributes (owner, group and mode) to different values, *even if* one of them would not modify the attribute at all. - The ``path`` attribute of the ``Permissions`` are completely ignored, as they are understood to apply to the same file of interest through a previous call of ``filter_permissions()``. :param left: the first set of permissions. :param right: the second set of permissions. """left=leftor[]right=rightor[]iflen(left)==0orlen(right)==0:# If either (or both) of the lists are empty, consider them "compatible".returnTrue# Otherwise, "squash" both lists into individual Permissions objects to# compare them.squashed_left=_squash_permissions(left)squashed_right=_squash_permissions(right)ifsquashed_left.owner!=squashed_right.owner:returnFalseifsquashed_left.group!=squashed_right.group:returnFalseifsquashed_left.modeisNoneandsquashed_right.modeisNone:returnTrueifsquashed_left.modeisNoneorsquashed_right.modeisNone:returnFalsereturnsquashed_left.mode_octal==squashed_right.mode_octal
def_squash_permissions(permissions:list[Permissions])->Permissions:"""Compress a sequence of Permissions into a single one. This function produces a single ``Permissions`` object whose application to a path is equivalent to calling ``apply_permissions()`` with the full list ``permissions``. Note that the ``path`` attribute of the Permissions objects are ignored, as they are assumed to all match (so they must have been pre-filtered with ``filter_permissions``). :param permissions: A series of Permissions objects to be "squashed" into a single one. """attributes={"path":"*","owner":None,"group":None,"mode":None,}keys=tuple(attributes.keys())forpermissioninpermissions:forkeyinkeys:permission_value=getattr(permission,key)ifpermission_valueisnotNone:attributes[key]=permission_valuereturnPermissions(**attributes)