mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 03:12:14 +01:00
feat(pdfengines): support owner-only encryption and document permissions
This commit is contained in:
@@ -53,7 +53,7 @@ type PdfEngineMock struct {
|
||||
PageCountMock func(ctx context.Context, logger *slog.Logger, inputPath string) (int, error)
|
||||
WriteMetadataMock func(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error
|
||||
ReadBookmarksMock func(ctx context.Context, logger *slog.Logger, inputPath string) ([]Bookmark, error)
|
||||
EncryptMock func(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error
|
||||
EncryptMock func(ctx context.Context, logger *slog.Logger, inputPath string, opts EncryptOptions) error
|
||||
EmbedFilesMock func(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error
|
||||
EmbedFilesMetadataMock func(ctx context.Context, logger *slog.Logger, metadata map[string]map[string]string, inputPath string) error
|
||||
WriteBookmarksMock func(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []Bookmark) error
|
||||
@@ -96,8 +96,8 @@ func (engine *PdfEngineMock) ReadBookmarks(ctx context.Context, logger *slog.Log
|
||||
return engine.ReadBookmarksMock(ctx, logger, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return engine.EncryptMock(ctx, logger, inputPath, userPassword, ownerPassword)
|
||||
func (engine *PdfEngineMock) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts EncryptOptions) error {
|
||||
return engine.EncryptMock(ctx, logger, inputPath, opts)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
|
||||
|
||||
@@ -147,6 +147,56 @@ type PdfFormats struct {
|
||||
PdfUa bool
|
||||
}
|
||||
|
||||
// PdfPermissions gathers the document permissions enforced when a PDF is
|
||||
// encrypted. Each field defaults to true (the action is allowed); set a field
|
||||
// to false to restrict it. Restrictions are advisory: viewers honor them, but
|
||||
// they are not cryptographically enforced once the document opens.
|
||||
type PdfPermissions struct {
|
||||
// AllowPrinting permits printing the document.
|
||||
AllowPrinting bool
|
||||
|
||||
// AllowCopying permits extracting text and graphics.
|
||||
AllowCopying bool
|
||||
|
||||
// AllowModifying permits changing the document content.
|
||||
AllowModifying bool
|
||||
|
||||
// AllowAnnotating permits adding or modifying annotations.
|
||||
AllowAnnotating bool
|
||||
|
||||
// AllowFillingForms permits filling in form fields.
|
||||
AllowFillingForms bool
|
||||
|
||||
// AllowAssembling permits inserting, deleting, and rotating pages.
|
||||
AllowAssembling bool
|
||||
}
|
||||
|
||||
// Restricted reports whether at least one permission is denied.
|
||||
func (p PdfPermissions) Restricted() bool {
|
||||
return !p.AllowPrinting ||
|
||||
!p.AllowCopying ||
|
||||
!p.AllowModifying ||
|
||||
!p.AllowAnnotating ||
|
||||
!p.AllowFillingForms ||
|
||||
!p.AllowAssembling
|
||||
}
|
||||
|
||||
// EncryptOptions gathers the parameters for encrypting a PDF. An empty
|
||||
// UserPassword with a set OwnerPassword produces an owner-only document: it
|
||||
// opens without a password but enforces the [PdfPermissions].
|
||||
type EncryptOptions struct {
|
||||
// UserPassword is required to open the document. Empty means no open
|
||||
// password.
|
||||
UserPassword string
|
||||
|
||||
// OwnerPassword grants full access (lifts the permission restrictions).
|
||||
// When empty, it defaults to UserPassword.
|
||||
OwnerPassword string
|
||||
|
||||
// Permissions are the actions allowed when opened with the user password.
|
||||
Permissions PdfPermissions
|
||||
}
|
||||
|
||||
// Bookmark represents a node in the PDF document's outline
|
||||
// (table of contents).
|
||||
type Bookmark struct {
|
||||
@@ -247,11 +297,11 @@ type PdfEngine interface {
|
||||
// The bookmarks parameter represents the hierarchical tree of the outline.
|
||||
WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []Bookmark) error
|
||||
|
||||
// Encrypt adds password protection to a PDF file.
|
||||
// The userPassword is required to open the document.
|
||||
// The ownerPassword provides full access to the document.
|
||||
// If the ownerPassword is empty, it defaults to the userPassword.
|
||||
Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error
|
||||
// Encrypt adds password protection and permission restrictions to a PDF
|
||||
// file, as described by [EncryptOptions]. An empty user password with a set
|
||||
// owner password yields an owner-only document (opens without a password,
|
||||
// permissions enforced).
|
||||
Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts EncryptOptions) error
|
||||
|
||||
// EmbedFiles embeds files into a PDF. All files are embedded as file attachments
|
||||
// without modifying the main PDF content.
|
||||
|
||||
@@ -446,7 +446,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
mode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
encrypt := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
@@ -478,7 +478,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate stamp: %w", err)
|
||||
}
|
||||
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, embedsMetadata, facturX, facturxXmlPath, watermark, stamp, rotateAngle, rotatePages)
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, encrypt, embedPaths, embedsMetadata, facturX, facturxXmlPath, watermark, stamp, rotateAngle, rotatePages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||
}
|
||||
@@ -535,7 +535,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
mode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
encrypt := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
@@ -564,7 +564,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
|
||||
url := fmt.Sprintf("file://%s", inputPath)
|
||||
options.AllowedFilePrefixes = []string{ctx.DirPath()}
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, embedsMetadata, facturX, facturxXmlPath, watermark, stamp, rotateAngle, rotatePages)
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, encrypt, embedPaths, embedsMetadata, facturX, facturxXmlPath, watermark, stamp, rotateAngle, rotatePages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||
}
|
||||
@@ -618,7 +618,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
mode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
encrypt := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
@@ -656,7 +656,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
|
||||
options.AllowedFilePrefixes = []string{ctx.DirPath()}
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, embedsMetadata, facturX, facturxXmlPath, watermark, stamp, rotateAngle, rotatePages)
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, encrypt, embedPaths, embedsMetadata, facturX, facturxXmlPath, watermark, stamp, rotateAngle, rotatePages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||
}
|
||||
@@ -781,7 +781,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
|
||||
return fmt.Sprintf("file://%s", inputPath), nil
|
||||
}
|
||||
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]any, userPassword, ownerPassword string, embedPaths []string, embedsMetadata map[string]map[string]string, facturX gotenberg.FacturX, facturxXmlPath string, watermark, stamp gotenberg.Stamp, rotateAngle int, rotatePages string) error {
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]any, encrypt gotenberg.EncryptOptions, embedPaths []string, embedsMetadata map[string]map[string]string, facturX gotenberg.FacturX, facturxXmlPath string, watermark, stamp gotenberg.Stamp, rotateAngle int, rotatePages string) error {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1130.
|
||||
filename := ctx.OutputFilename(outputPath)
|
||||
@@ -843,7 +843,12 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
||||
return fmt.Errorf("convert to PDF: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
err = pdfengines.ValidatePdfFormatsCompat(pdfFormats, encrypt.UserPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = pdfengines.ValidatePdfEncryptCompat(encrypt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -902,7 +907,7 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
||||
return fmt.Errorf("apply Factur-X: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.EncryptPdfStub(ctx, engine, userPassword, ownerPassword, convertOutputPaths)
|
||||
err = pdfengines.EncryptPdfStub(ctx, engine, encrypt, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *slog.Logger,
|
||||
}
|
||||
|
||||
// Encrypt is not available in this implementation.
|
||||
func (engine *ExifTool) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
func (engine *ExifTool) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
_, span := gotenberg.Tracer().Start(ctx, "exiftool.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
|
||||
@@ -106,7 +106,7 @@ func (engine *LibreOfficePdfEngine) ReadBookmarks(ctx context.Context, logger *s
|
||||
}
|
||||
|
||||
// Encrypt is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
func (engine *LibreOfficePdfEngine) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
return fmt.Errorf("encrypt PDF using LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
splitMode := pdfengines.FormDataPdfSplitMode(form, false)
|
||||
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
encrypt := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
@@ -314,7 +314,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
return fmt.Errorf("validate stamp: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
err = pdfengines.ValidatePdfFormatsCompat(pdfFormats, encrypt.UserPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = pdfengines.ValidatePdfEncryptCompat(encrypt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -518,7 +523,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
return fmt.Errorf("apply Factur-X: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.EncryptPdfStub(ctx, engine, userPassword, ownerPassword, outputPaths)
|
||||
err = pdfengines.EncryptPdfStub(ctx, engine, encrypt, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
}
|
||||
|
||||
@@ -503,30 +503,38 @@ func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *slog.Logger, fileP
|
||||
}
|
||||
|
||||
// Encrypt adds password protection to a PDF file using pdfcpu.
|
||||
func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
func (engine *PdfCpu) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdfcpu.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
if userPassword == "" {
|
||||
err := errors.New("user password cannot be empty")
|
||||
ownerPassword := opts.OwnerPassword
|
||||
if ownerPassword == "" {
|
||||
ownerPassword = opts.UserPassword
|
||||
}
|
||||
|
||||
// An empty user password is allowed: it produces an owner-only document.
|
||||
if opts.UserPassword == "" && ownerPassword == "" {
|
||||
err := errors.New("at least a user or owner password is required")
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if ownerPassword == "" {
|
||||
ownerPassword = userPassword
|
||||
// pdfcpu only supports coarse permissions: all actions or none.
|
||||
perm := "all"
|
||||
if opts.Permissions.Restricted() {
|
||||
perm = "none"
|
||||
}
|
||||
|
||||
args := make([]string, 0, 11)
|
||||
args = append(args, "encrypt")
|
||||
args = append(args, "--mode", "aes")
|
||||
args = append(args, "--upw", userPassword)
|
||||
args = append(args, "--upw", opts.UserPassword)
|
||||
args = append(args, "--opw", ownerPassword)
|
||||
args = append(args, "--perm", "all")
|
||||
args = append(args, "--perm", perm)
|
||||
args = append(args, inputPath, inputPath)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
|
||||
@@ -259,10 +259,10 @@ func (multi *multiPdfEngines) WriteBookmarks(ctx context.Context, logger *slog.L
|
||||
|
||||
// Encrypt adds password protection to a PDF file using the first available
|
||||
// engine that supports password protection.
|
||||
func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
return runWithFallbackVoid(ctx, "pdfengines.Encrypt", multi.passwordEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.Encrypt(ctx, logger, inputPath, userPassword, ownerPassword)
|
||||
return engine.Encrypt(ctx, logger, inputPath, opts)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("encrypt PDF using multi PDF engines: %w", err) },
|
||||
)
|
||||
|
||||
@@ -667,21 +667,44 @@ func InjectFacturXXMPStub(ctx *api.Context, engine gotenberg.PdfEngine, facturX
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormDataPdfEncrypt extracts encryption parameters from form data.
|
||||
func FormDataPdfEncrypt(form *api.FormData) (userPassword, ownerPassword string) {
|
||||
form.String("userPassword", &userPassword, "")
|
||||
form.String("ownerPassword", &ownerPassword, "")
|
||||
return userPassword, ownerPassword
|
||||
// FormDataPdfEncrypt extracts the encryption parameters and permissions from
|
||||
// form data. Permissions default to allowed.
|
||||
func FormDataPdfEncrypt(form *api.FormData) gotenberg.EncryptOptions {
|
||||
var opts gotenberg.EncryptOptions
|
||||
form.
|
||||
String("userPassword", &opts.UserPassword, "").
|
||||
String("ownerPassword", &opts.OwnerPassword, "").
|
||||
Bool("allowPrinting", &opts.Permissions.AllowPrinting, true).
|
||||
Bool("allowCopying", &opts.Permissions.AllowCopying, true).
|
||||
Bool("allowModifying", &opts.Permissions.AllowModifying, true).
|
||||
Bool("allowAnnotating", &opts.Permissions.AllowAnnotating, true).
|
||||
Bool("allowFillingForms", &opts.Permissions.AllowFillingForms, true).
|
||||
Bool("allowAssembling", &opts.Permissions.AllowAssembling, true)
|
||||
return opts
|
||||
}
|
||||
|
||||
// EncryptPdfStub adds password protection to PDF files.
|
||||
func EncryptPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, userPassword, ownerPassword string, inputPaths []string) error {
|
||||
if userPassword == "" {
|
||||
// ValidatePdfEncryptCompat returns a 400 error when permission restrictions are
|
||||
// requested without a password to anchor them.
|
||||
func ValidatePdfEncryptCompat(opts gotenberg.EncryptOptions) error {
|
||||
if opts.Permissions.Restricted() && opts.UserPassword == "" && opts.OwnerPassword == "" {
|
||||
return api.WrapError(
|
||||
errors.New("permission restrictions require a password"),
|
||||
api.NewSentinelHttpError(http.StatusBadRequest, "Invalid form data: permission restrictions require a 'userPassword' or 'ownerPassword'"),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncryptPdfStub adds password protection and permission restrictions to PDF
|
||||
// files. It does nothing when no password is provided.
|
||||
func EncryptPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, opts gotenberg.EncryptOptions, inputPaths []string) error {
|
||||
if opts.UserPassword == "" && opts.OwnerPassword == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.Encrypt(ctx, ctx.Log(), inputPath, userPassword, ownerPassword)
|
||||
err := engine.Encrypt(ctx, ctx.Log(), inputPath, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDF '%s': %w", inputPath, err)
|
||||
}
|
||||
@@ -899,7 +922,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
bookmarks := FormDataPdfBookmarks(form, false)
|
||||
userPassword, ownerPassword := FormDataPdfEncrypt(form)
|
||||
encrypt := FormDataPdfEncrypt(form)
|
||||
embedPaths := FormDataPdfEmbeds(form)
|
||||
watermark := FormDataPdfWatermark(form, false)
|
||||
watermarkFile := FormDataPdfWatermarkFile(form)
|
||||
@@ -930,7 +953,12 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate stamp: %w", err)
|
||||
}
|
||||
|
||||
err = ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
err = ValidatePdfFormatsCompat(pdfFormats, encrypt.UserPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ValidatePdfEncryptCompat(encrypt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1043,7 +1071,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("apply Factur-X: %w", err)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, outputPaths)
|
||||
err = EncryptPdfStub(ctx, engine, encrypt, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
}
|
||||
@@ -1071,7 +1099,7 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
mode := FormDataPdfSplitMode(form, true)
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := FormDataPdfEncrypt(form)
|
||||
encrypt := FormDataPdfEncrypt(form)
|
||||
embedPaths := FormDataPdfEmbeds(form)
|
||||
watermark := FormDataPdfWatermark(form, false)
|
||||
watermarkFile := FormDataPdfWatermarkFile(form)
|
||||
@@ -1100,7 +1128,12 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate stamp: %w", err)
|
||||
}
|
||||
|
||||
err = ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
err = ValidatePdfFormatsCompat(pdfFormats, encrypt.UserPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ValidatePdfEncryptCompat(encrypt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1166,7 +1199,7 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("apply Factur-X: %w", err)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, convertOutputPaths)
|
||||
err = EncryptPdfStub(ctx, engine, encrypt, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
}
|
||||
@@ -1450,20 +1483,26 @@ func encryptRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
encrypt := FormDataPdfEncrypt(form)
|
||||
|
||||
var inputPaths []string
|
||||
var userPassword string
|
||||
var ownerPassword string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
MandatoryString("userPassword", &userPassword).
|
||||
String("ownerPassword", &ownerPassword, "").
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, inputPaths)
|
||||
// At least one password is required; an empty user password with an
|
||||
// owner password yields an owner-only document.
|
||||
if encrypt.UserPassword == "" && encrypt.OwnerPassword == "" {
|
||||
return api.WrapError(
|
||||
errors.New("no password provided"),
|
||||
api.NewSentinelHttpError(http.StatusBadRequest, "Invalid form data: a 'userPassword' or 'ownerPassword' is required"),
|
||||
)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, encrypt, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
}
|
||||
|
||||
@@ -259,21 +259,21 @@ func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *slog.Logger, inp
|
||||
}
|
||||
|
||||
// Encrypt adds password protection to a PDF file using PDFtk.
|
||||
func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "pdftk.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
if userPassword == "" {
|
||||
err := errors.New("user password cannot be empty")
|
||||
if opts.UserPassword == "" || opts.Permissions.Restricted() {
|
||||
err := gotenberg.NewPdfEngineInvalidArgs("pdftk", "owner-only encryption and permission restrictions are not supported; consider switching to another PDF engine (e.g. qpdf)")
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if ownerPassword == userPassword || ownerPassword == "" {
|
||||
if opts.OwnerPassword == opts.UserPassword || opts.OwnerPassword == "" {
|
||||
err := gotenberg.NewPdfEngineInvalidArgs("pdftk", "both 'userPassword' and 'ownerPassword' must be provided and different. Consider switching to another PDF engine if this behavior does not work with your workflow")
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
@@ -287,8 +287,8 @@ func (engine *PdfTk) Encrypt(ctx context.Context, logger *slog.Logger, inputPath
|
||||
args = append(args, inputPath)
|
||||
args = append(args, "output", tmpPath)
|
||||
args = append(args, "encrypt_128bit")
|
||||
args = append(args, "user_pw", userPassword)
|
||||
args = append(args, "owner_pw", ownerPassword)
|
||||
args = append(args, "user_pw", opts.UserPassword)
|
||||
args = append(args, "owner_pw", opts.OwnerPassword)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
|
||||
76
pkg/modules/qpdf/encrypt_test.go
Normal file
76
pkg/modules/qpdf/encrypt_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package qpdf
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestQpdfPermissionArgs(t *testing.T) {
|
||||
allAllowed := gotenberg.PdfPermissions{
|
||||
AllowPrinting: true,
|
||||
AllowCopying: true,
|
||||
AllowModifying: true,
|
||||
AllowAnnotating: true,
|
||||
AllowFillingForms: true,
|
||||
AllowAssembling: true,
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
perms gotenberg.PdfPermissions
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
scenario: "all allowed yields no flags",
|
||||
perms: allAllowed,
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
scenario: "printing denied",
|
||||
perms: gotenberg.PdfPermissions{
|
||||
AllowPrinting: false,
|
||||
AllowCopying: true,
|
||||
AllowModifying: true,
|
||||
AllowAnnotating: true,
|
||||
AllowFillingForms: true,
|
||||
AllowAssembling: true,
|
||||
},
|
||||
expect: []string{
|
||||
"--print=none",
|
||||
"--extract=y",
|
||||
"--modify-other=y",
|
||||
"--annotate=y",
|
||||
"--form=y",
|
||||
"--assemble=y",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "copying denied",
|
||||
perms: gotenberg.PdfPermissions{
|
||||
AllowPrinting: true,
|
||||
AllowCopying: false,
|
||||
AllowModifying: true,
|
||||
AllowAnnotating: true,
|
||||
AllowFillingForms: true,
|
||||
AllowAssembling: true,
|
||||
},
|
||||
expect: []string{
|
||||
"--print=full",
|
||||
"--extract=n",
|
||||
"--modify-other=y",
|
||||
"--annotate=y",
|
||||
"--form=y",
|
||||
"--assemble=y",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
got := qpdfPermissionArgs(tc.perms)
|
||||
if !reflect.DeepEqual(got, tc.expect) {
|
||||
t.Errorf("expected %v but got %v", tc.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -295,29 +295,63 @@ func (engine *QPdf) ReadBookmarks(ctx context.Context, logger *slog.Logger, inpu
|
||||
}
|
||||
|
||||
// Encrypt adds password protection to a PDF file using QPDF.
|
||||
func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
// qpdfPermissionArgs maps PDF permissions to QPDF --encrypt restriction flags.
|
||||
// It returns nil when every permission is allowed, matching QPDF's default
|
||||
// (all actions permitted).
|
||||
func qpdfPermissionArgs(p gotenberg.PdfPermissions) []string {
|
||||
if !p.Restricted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
yn := func(allowed bool) string {
|
||||
if allowed {
|
||||
return "y"
|
||||
}
|
||||
return "n"
|
||||
}
|
||||
|
||||
print := "full"
|
||||
if !p.AllowPrinting {
|
||||
print = "none"
|
||||
}
|
||||
|
||||
return []string{
|
||||
"--print=" + print,
|
||||
"--extract=" + yn(p.AllowCopying),
|
||||
"--modify-other=" + yn(p.AllowModifying),
|
||||
"--annotate=" + yn(p.AllowAnnotating),
|
||||
"--form=" + yn(p.AllowFillingForms),
|
||||
"--assemble=" + yn(p.AllowAssembling),
|
||||
}
|
||||
}
|
||||
|
||||
func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath string, opts gotenberg.EncryptOptions) error {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Encrypt",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(semconv.ServerAddress(engine.binPath)),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
if userPassword == "" {
|
||||
err := errors.New("user password cannot be empty")
|
||||
ownerPassword := opts.OwnerPassword
|
||||
if ownerPassword == "" {
|
||||
ownerPassword = opts.UserPassword
|
||||
}
|
||||
|
||||
// An empty user password is allowed: it produces an owner-only document.
|
||||
if opts.UserPassword == "" && ownerPassword == "" {
|
||||
err := errors.New("at least a user or owner password is required")
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if ownerPassword == "" {
|
||||
ownerPassword = userPassword
|
||||
}
|
||||
|
||||
args := make([]string, 0, 7+len(engine.globalArgs))
|
||||
args := make([]string, 0, 14+len(engine.globalArgs))
|
||||
args = append(args, inputPath)
|
||||
args = append(args, engine.globalArgs...)
|
||||
args = append(args, "--replace-input")
|
||||
args = append(args, "--encrypt", userPassword, ownerPassword, "256", "--")
|
||||
args = append(args, "--encrypt", opts.UserPassword, ownerPassword, "256")
|
||||
args = append(args, qpdfPermissionArgs(opts.Permissions)...)
|
||||
args = append(args, "--")
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user