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

ENH: ease Permissions readability/writability #2397

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 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
22 changes: 20 additions & 2 deletions pypdf/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -1808,8 +1808,26 @@
# TODO: raise Exception for wrong password
return self._encryption.verify(password)

def decode_permissions(self, permissions_code: int) -> Dict[str, bool]:
# Takes the permissions as an integer, returns the allowed access
def decode_permissions(self, permissions_code: Optional[int]) -> Dict[str, bool]:
"""
Decodes the permissions as a dictionary

Args:
permissions_code: integer providing bits from an integer
if None is provided, the value is retrieved from the default pdf fields
Individual permissions using PKCS#7 shall be extracted manually

Returns:
permissions as human readable structure in a dict
Can be used to feed permissions_flag within PdfWriter.encrypt
"""
if permissions_code is None:
permissions_code = cast(

Check warning on line 1825 in pypdf/_reader.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_reader.py#L1825

Added line #L1825 was not covered by tests
int,
cast(DictionaryObject, self.trailer[TK.ENCRYPT]).get("/P", -1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use self._encryption.P here?

if TK.ENCRYPT in self.trailer
else -1,
)
permissions = {}
permissions["print"] = permissions_code & (1 << 3 - 1) != 0 # bit 3
permissions["modify"] = permissions_code & (1 << 4 - 1) != 0 # bit 4
Expand Down
28 changes: 26 additions & 2 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
)

OPTIONAL_READ_WRITE_FIELD = FieldFlag(0)
ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions((2**31 - 1) - 3)
ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions((2**32 - 1) - 3)


class ObjectDeletionFlag(enum.IntFlag):
Expand Down Expand Up @@ -1065,7 +1065,9 @@
user_password: str,
owner_password: Optional[str] = None,
use_128bit: bool = True,
permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS,
permissions_flag: Union[
UserAccessPermissions, Dict[str, bool]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we really complicate this API further? We cannot we just provide a convenience method on UserAccessPermissions instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also prefer not to add the string versions here. I regret adding them in the first place 🙈

] = ALL_DOCUMENT_PERMISSIONS,
*,
algorithm: Optional[str] = None,
) -> None:
Expand All @@ -1088,13 +1090,35 @@
Bit position 3 is for printing, 4 is for modifying content,
5 and 6 control annotations, 9 for form fields,
10 for extraction of text and graphics.
permissions can be provided as an integer or as a dictionary
as provided by `PdfReader.decode_permissions()`
algorithm: encrypt algorithm. Values maybe one of "RC4-40", "RC4-128",
"AES-128", "AES-256-R5", "AES-256". If it's valid,
`use_128bit` will be ignored.
"""
if owner_password is None:
owner_password = user_password

if isinstance(permissions_flag, dict):
permissions = permissions_flag
_flag = cast(int, ALL_DOCUMENT_PERMISSIONS)

Check warning on line 1104 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1103-L1104

Added lines #L1103 - L1104 were not covered by tests
if not permissions.get("print", False):
_flag -= 1 << 3

Check warning on line 1106 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1106

Added line #L1106 was not covered by tests
if not permissions.get("modify", False):
_flag -= 1 << 4

Check warning on line 1108 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1108

Added line #L1108 was not covered by tests
if not permissions.get("copy", False):
_flag -= 1 << 5

Check warning on line 1110 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1110

Added line #L1110 was not covered by tests
if not permissions.get("annotations", False):
_flag -= 1 << 6

Check warning on line 1112 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1112

Added line #L1112 was not covered by tests
if not permissions.get("forms", False):
_flag -= 1 << 9

Check warning on line 1114 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1114

Added line #L1114 was not covered by tests
if not permissions.get("accessability", False):
_flag -= 1 << 10

Check warning on line 1116 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1116

Added line #L1116 was not covered by tests
if not permissions.get("assemble", False):
_flag -= 1 << 11

Check warning on line 1118 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1118

Added line #L1118 was not covered by tests
if not permissions.get("print_high_quality", False):
_flag -= 1 << 12
permissions_flag = cast(UserAccessPermissions, _flag)

Check warning on line 1121 in pypdf/_writer.py

View check run for this annotation

Codecov / codecov/patch

pypdf/_writer.py#L1120-L1121

Added lines #L1120 - L1121 were not covered by tests
if algorithm is not None:
try:
alg = getattr(EncryptAlgorithm, algorithm.replace("-", "_"))
Expand Down
16 changes: 9 additions & 7 deletions tests/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ def writer_operate(writer: PdfWriter) -> None:
)
def test_writer_operations_by_traditional_usage(convert, needs_cleanup):
if callable(convert):
write_data_here = convert(NamedTemporaryFile(suffix=".pdf", delete=False).name)
with NamedTemporaryFile(suffix=".pdf", delete=False) as fo:
write_data_here = convert(fo.name)
else:
write_data_here = convert

Expand Down Expand Up @@ -254,7 +255,8 @@ def test_writer_operations_by_traditional_usage(convert, needs_cleanup):
)
def test_writer_operations_by_semi_traditional_usage(convert, needs_cleanup):
if callable(convert):
write_data_here = convert(NamedTemporaryFile(suffix=".pdf", delete=False).name)
with NamedTemporaryFile(suffix=".pdf", delete=False) as fo:
write_data_here = convert(fo.name)
else:
write_data_here = convert

Expand All @@ -281,11 +283,10 @@ def test_writer_operations_by_semi_traditional_usage(convert, needs_cleanup):
(BytesIO(), False),
],
)
def test_writer_operations_by_semi_new_traditional_usage(
convert, needs_cleanup
):
def test_writer_operations_by_semi_new_traditional_usage(convert, needs_cleanup):
if callable(convert):
write_data_here = convert(NamedTemporaryFile(suffix=".pdf", delete=False).name)
with NamedTemporaryFile(suffix=".pdf", delete=False) as fo:
write_data_here = convert(fo.name)
else:
write_data_here = convert

Expand All @@ -309,7 +310,8 @@ def test_writer_operations_by_semi_new_traditional_usage(
)
def test_writer_operation_by_new_usage(convert, needs_cleanup):
if callable(convert):
write_data_here = convert(NamedTemporaryFile(suffix=".pdf", delete=False).name)
with NamedTemporaryFile(suffix=".pdf", delete=False) as fo:
write_data_here = convert(fo.name)
else:
write_data_here = convert

Expand Down
Loading