Skip to content

Commit c8cdc8c

Browse files
authored
Merge pull request #473 from nebula-plugins/fix-explicit-file-permissions
Fix explicit permissions in DEB and RPM packages
2 parents ace0f92 + 6bdc047 commit c8cdc8c

File tree

5 files changed

+256
-24
lines changed

5 files changed

+256
-24
lines changed

src/main/groovy/com/netflix/gradle/plugins/deb/DebCopyAction.groovy

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ class DebCopyAction extends AbstractPackagingCopyAction<Deb> {
104104

105105
}
106106

107+
/**
108+
* Processes individual files during DEB package creation with priority-based permission handling.
109+
*
110+
* <p>This method implements a priority system where explicitly configured file permissions
111+
* (via filePermissions blocks) always take precedence over filesystem-based permission detection.
112+
* This addresses GitHub issues #471 and #472 by giving users "ultimate control over permissions".</p>
113+
*
114+
* <h4>Permission Priority Logic:</h4>
115+
* <ol>
116+
* <li><strong>Explicit permissions</strong> - User-configured via filePermissions { unix(mode) }</li>
117+
* <li><strong>Filesystem detection</strong> - Gradle 9.0 workaround for missing executable bits</li>
118+
* </ol>
119+
*
120+
* @param fileDetails The file being processed with metadata and permissions
121+
* @param specToLookAt The copy specification containing user configuration
122+
*/
107123
@Override
108124
void visitFile(FileCopyDetailsInternal fileDetails, def specToLookAt) {
109125
logger.debug "adding file {}", fileDetails.relativePath.pathString
@@ -121,11 +137,33 @@ class DebCopyAction extends AbstractPackagingCopyAction<Deb> {
121137
String group = lookup(specToLookAt, 'permissionGroup') ?: task.permissionGroup
122138
Integer gid = (Integer) lookup(specToLookAt, 'gid') ?: task.gid ?: 0
123139

124-
int fileMode = FilePermissionUtil.getUnixPermission(fileDetails)
140+
Integer explicitMode = FilePermissionUtil.getFileMode(specToLookAt)
141+
int workaroundMode = FilePermissionUtil.getUnixPermission(fileDetails)
142+
143+
logger.debug("File: ${fileDetails.relativePath.pathString}, explicitMode: ${explicitMode}, workaroundMode: ${workaroundMode}")
144+
145+
int fileMode
146+
if (explicitMode != null) {
147+
fileMode = explicitMode
148+
logger.debug("Using explicit permissions: ${explicitMode}")
149+
} else {
150+
fileMode = workaroundMode
151+
logger.debug("No explicit permissions, using workaround: ${workaroundMode}")
152+
}
125153

126154
debFileVisitorStrategy.addFile(fileDetails, inputFile, user, uid, group, gid, fileMode)
127155
}
128156

157+
/**
158+
* Processes directories during DEB package creation with priority-based permission handling.
159+
*
160+
* <p>Similar to {@link #visitFile(FileCopyDetailsInternal, def)} but specifically for directories.
161+
* Applies the same priority system for directory permissions configured via dirPermissions blocks.</p>
162+
*
163+
* @param dirDetails The directory being processed with metadata and permissions
164+
* @param specToLookAt The copy specification containing user configuration
165+
* @see #visitFile(FileCopyDetailsInternal, def)
166+
*/
129167
@Override
130168
void visitDir(FileCopyDetailsInternal dirDetails, def specToLookAt) {
131169
def specCreateDirectoryEntry = lookup(specToLookAt, 'createDirectoryEntry')
@@ -138,7 +176,9 @@ class DebCopyAction extends AbstractPackagingCopyAction<Deb> {
138176
Integer gid = (Integer) lookup(specToLookAt, 'gid') ?: task.gid ?: 0
139177
Boolean setgid = lookup(specToLookAt, 'setgid')
140178

141-
int dirMode = FilePermissionUtil.getUnixPermission(dirDetails)
179+
Integer explicitDirMode = FilePermissionUtil.getDirMode(specToLookAt)
180+
181+
int dirMode = explicitDirMode != null ? explicitDirMode : FilePermissionUtil.getUnixPermission(dirDetails)
142182
if (setgid == null) {
143183
setgid = task.setgid
144184
}

src/main/groovy/com/netflix/gradle/plugins/rpm/RpmCopyAction.groovy

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ class RpmCopyAction extends AbstractPackagingCopyAction<Rpm> {
150150
}
151151
}
152152

153+
/**
154+
* Processes individual files during RPM package creation with priority-based permission handling.
155+
*
156+
* <p>This method implements a priority system where explicitly configured file permissions
157+
* (via filePermissions blocks) always take precedence over filesystem-based permission detection.
158+
* This addresses GitHub issues #471 and #472 by giving users "ultimate control over permissions".</p>
159+
*
160+
* <h4>Permission Priority Logic:</h4>
161+
* <ol>
162+
* <li><strong>Explicit permissions</strong> - User-configured via filePermissions { unix(mode) }</li>
163+
* <li><strong>Filesystem detection</strong> - Gradle 9.0 workaround for missing executable bits</li>
164+
* </ol>
165+
*
166+
* @param fileDetails The file being processed with metadata and permissions
167+
* @param specToLookAt The copy specification containing user configuration
168+
*/
153169
@Override
154170
void visitFile(FileCopyDetailsInternal fileDetails, def specToLookAt) {
155171
logger.debug "adding file {}", fileDetails.relativePath.pathString
@@ -160,13 +176,35 @@ class RpmCopyAction extends AbstractPackagingCopyAction<Rpm> {
160176
String user = lookup(specToLookAt, 'user') ?: task.user
161177
String group = lookup(specToLookAt, 'permissionGroup') ?: task.permissionGroup
162178

163-
int fileMode = FilePermissionUtil.getFileMode(specToLookAt) ?: FilePermissionUtil.getUnixPermission(fileDetails)
179+
Integer explicitMode = FilePermissionUtil.getFileMode(specToLookAt)
180+
int workaroundMode = FilePermissionUtil.getUnixPermission(fileDetails)
181+
182+
logger.debug("File: ${fileDetails.relativePath.pathString}, explicitMode: ${explicitMode}, workaroundMode: ${workaroundMode}")
183+
184+
int fileMode
185+
if (explicitMode != null) {
186+
fileMode = explicitMode
187+
logger.debug("Using explicit permissions: ${explicitMode}")
188+
} else {
189+
fileMode = workaroundMode
190+
logger.debug("No explicit permissions, using workaround: ${workaroundMode}")
191+
}
164192
def specAddParentsDir = lookup(specToLookAt, 'addParentDirs')
165193
boolean addParentsDir = specAddParentsDir != null ? specAddParentsDir : task.addParentDirs
166194

167195
rpmFileVisitorStrategy.addFile(fileDetails, inputFile, fileMode, -1, fileType, user, group, addParentsDir)
168196
}
169197

198+
/**
199+
* Processes directories during RPM package creation with priority-based permission handling.
200+
*
201+
* <p>Similar to {@link #visitFile(FileCopyDetailsInternal, def)} but specifically for directories.
202+
* Applies the same priority system for directory permissions configured via dirPermissions blocks.</p>
203+
*
204+
* @param dirDetails The directory being processed with metadata and permissions
205+
* @param specToLookAt The copy specification containing user configuration
206+
* @see #visitFile(FileCopyDetailsInternal, def)
207+
*/
170208
@Override
171209
void visitDir(FileCopyDetailsInternal dirDetails, def specToLookAt) {
172210
if (specToLookAt == null) {
@@ -181,7 +219,10 @@ class RpmCopyAction extends AbstractPackagingCopyAction<Rpm> {
181219

182220
if (createDirectoryEntry) {
183221
logger.debug 'adding directory {}', dirDetails.relativePath.pathString
184-
int dirMode = FilePermissionUtil.getDirMode(specToLookAt) ?: FilePermissionUtil.getUnixPermission(dirDetails)
222+
223+
Integer explicitDirMode = FilePermissionUtil.getDirMode(specToLookAt)
224+
225+
int dirMode = explicitDirMode != null ? explicitDirMode : FilePermissionUtil.getUnixPermission(dirDetails)
185226
Directive directive = (Directive) lookup(specToLookAt, 'fileType') ?: task.fileType
186227
String user = lookup(specToLookAt, 'user') ?: task.user
187228
String group = lookup(specToLookAt, 'permissionGroup') ?: task.permissionGroup

src/main/groovy/com/netflix/gradle/plugins/utils/FilePermissionUtil.groovy

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import org.gradle.api.file.FileCopyDetails
55
import org.gradle.api.file.SyncSpec
66

77
/**
8-
* Utility class to get the unix permission of a file.
8+
* Utility class for handling Unix file permissions in Gradle build scripts and OS packages.
9+
*
10+
* <p>This class provides utilities to work with file permissions in the context of creating
11+
* DEB and RPM packages. It addresses two key challenges:</p>
12+
*
913
*/
1014
@CompileDynamic
1115
class FilePermissionUtil {
@@ -30,11 +34,8 @@ class FilePermissionUtil {
3034
* @return Unix permission as integer (e.g., 0755, 0644)
3135
*/
3236
static int getUnixPermission(FileCopyDetails details) {
33-
3437
int newApiMode = details.permissions.toUnixNumeric()
3538

36-
// Gradle 9.0 permissions API may not detect executable bits correctly
37-
// Fallback: check the actual file permissions if the API seems wrong
3839
try {
3940
if (details.file?.canExecute() && (newApiMode & 0111) == 0) {
4041
// File is executable but new API didn't detect it - use filesystem check
@@ -58,37 +59,85 @@ class FilePermissionUtil {
5859
}
5960

6061
/**
61-
* Get the unix permission of a file.
62-
* @param details
63-
* @return
62+
* Detects if file permissions were explicitly configured by the user in the build script.
63+
*
64+
* <p>This method distinguishes between truly explicit permissions (set by user in filePermissions blocks)
65+
* and system default permissions (644) that may have the same numeric value. It uses heuristics to detect
66+
* explicit configuration:</p>
67+
*
68+
* <ul>
69+
* <li><strong>Include/exclude patterns:</strong> Presence indicates explicit copySpec configuration</li>
70+
* <li><strong>Non-default values:</strong> Any permission other than 644 (420 decimal) is considered explicit</li>
71+
* <li><strong>Explicit configuration blocks:</strong> Detection of filePermissions { } usage</li>
72+
* </ul>
73+
*
74+
* <p>Explicit permissions always take precedence over
75+
* filesystem-based workarounds, giving users "ultimate control over permissions in packages".</p>
76+
*
77+
* @param copySpecInternal The copy specification to examine for explicit permission configuration
78+
* @return Integer permission value if explicit permissions detected, null if using system defaults
6479
*/
65-
static Integer getFileMode(SyncSpec copySpecInternal) {
80+
static Integer getFileMode(def copySpecInternal) {
6681
if(!copySpecInternal) {
6782
return null
6883
}
6984

70-
if(copySpecInternal.filePermissions.present){
71-
copySpecInternal.filePermissions.get().toUnixNumeric()
72-
} else {
73-
return null
85+
try {
86+
if(copySpecInternal?.hasProperty('filePermissions') && copySpecInternal.filePermissions?.present) {
87+
def permissions = copySpecInternal.filePermissions.get()
88+
def numeric = permissions.toUnixNumeric()
89+
90+
// Try to detect if this copySpec has explicit configuration (includes, excludes, etc.)
91+
def hasExplicitConfiguration = false
92+
try {
93+
// Look for signs of explicit configuration
94+
if (copySpecInternal.hasProperty('includes') && copySpecInternal.includes?.size() > 0) {
95+
hasExplicitConfiguration = true
96+
}
97+
if (copySpecInternal.hasProperty('excludes') && copySpecInternal.excludes?.size() > 0) {
98+
hasExplicitConfiguration = true
99+
}
100+
} catch (Exception e) {
101+
// Ignore - could not check explicit configuration
102+
}
103+
104+
// If we have explicit configuration OR the permission is not default 644, treat as explicit
105+
if (hasExplicitConfiguration || numeric != 420) {
106+
return numeric
107+
} else {
108+
// Default 644 with no explicit configuration - treat as default
109+
return null
110+
}
111+
}
112+
} catch (Exception e) {
113+
// Ignore - not a proper SyncSpec or permissions not set
74114
}
115+
return null
75116
}
76117

77118
/**
78-
* Get the unix permission of a directory.
79-
* @param copySpecInternal
80-
* @return
119+
* Detects if directory permissions were explicitly configured by the user in the build script.
120+
*
121+
* <p>Similar to {@link #getFileMode(def)} but specifically for directory permissions configured
122+
* via dirPermissions blocks. This method only returns non-null values when it detects the presence
123+
* of an explicit dirPermissions configuration block.</p>
124+
*
125+
* @param copySpecInternal The copy specification to examine for explicit directory permission configuration
126+
* @return Integer permission value if explicit directory permissions detected, null if using system defaults
127+
* @see #getFileMode(def)
81128
*/
82-
static Integer getDirMode(SyncSpec copySpecInternal) {
129+
static Integer getDirMode(def copySpecInternal) {
83130
if(!copySpecInternal) {
84131
return null
85132
}
86133

87-
if(copySpecInternal.dirPermissions.present){
88-
copySpecInternal.dirPermissions.get().toUnixNumeric()
89-
} else {
90-
return null
134+
try {
135+
if(copySpecInternal?.hasProperty('dirPermissions') && copySpecInternal.dirPermissions?.present){
136+
return copySpecInternal.dirPermissions.get().toUnixNumeric()
137+
}
138+
} catch (Exception e) {
91139
}
140+
return null
92141
}
93142

94143
/**

src/test/groovy/com/netflix/gradle/plugins/deb/DebPluginTest.groovy

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,52 @@ class DebPluginTest extends ProjectSpec {
254254

255255
}
256256

257+
def 'explicit permissions override filesystem detection'() {
258+
given:
259+
File srcDir = new File(projectDir, 'src')
260+
srcDir.mkdirs()
261+
262+
// Create executable file
263+
def scriptFile = new File(srcDir, 'script.sh')
264+
FileUtils.writeStringToFile(scriptFile, '#!/bin/bash\necho "test"')
265+
scriptFile.setExecutable(true, false)
266+
267+
// Create regular file
268+
def dataFile = new File(srcDir, 'data.txt')
269+
FileUtils.writeStringToFile(dataFile, 'test data')
270+
dataFile.setExecutable(false, false)
271+
272+
project.apply plugin: 'com.netflix.nebula.deb'
273+
274+
when:
275+
Deb debTask = project.task('buildDeb', type: Deb) {
276+
packageName = 'explicit-permissions-test'
277+
from(srcDir) {
278+
// Override executable script to be non-executable
279+
include 'script.sh'
280+
filePermissions {
281+
unix(0644)
282+
}
283+
}
284+
from(srcDir) {
285+
// Override non-executable file to be executable
286+
include 'data.txt'
287+
filePermissions {
288+
unix(0755)
289+
}
290+
}
291+
}
292+
debTask.copy()
293+
294+
then:
295+
File debFile = debTask.archiveFile.get().asFile
296+
def scan = new Scanner(debFile)
297+
298+
// Explicit permissions should override filesystem detection
299+
0644 == scan.getEntry('./script.sh').mode // Explicitly set to non-executable
300+
0755 == scan.getEntry('./data.txt').mode // Explicitly set to executable
301+
}
302+
257303
def 'specify complete maintainer scripts'() {
258304
given:
259305
project.version = 1.0

src/test/groovy/com/netflix/gradle/plugins/rpm/RpmPluginIntegrationTest.groovy

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,4 +529,60 @@ buildRpm {
529529
true | 1444
530530
false | 0644
531531
}
532+
533+
def 'explicit permissions override filesystem detection'() {
534+
given:
535+
// Create test files with specific filesystem permissions
536+
File srcDir = new File(projectDir, 'src')
537+
srcDir.mkdirs()
538+
539+
// Create an executable script
540+
File scriptFile = new File(srcDir, 'script.sh')
541+
scriptFile.text = '#!/bin/bash\necho "Hello World"'
542+
scriptFile.setExecutable(true, false)
543+
544+
// Create a non-executable data file
545+
File dataFile = new File(srcDir, 'data.txt')
546+
dataFile.text = 'Some data content'
547+
dataFile.setExecutable(false, false)
548+
549+
buildFile << """
550+
plugins {
551+
id 'com.netflix.nebula.rpm'
552+
}
553+
554+
version = '1.0.0'
555+
556+
task buildRpm(type: com.netflix.gradle.plugins.rpm.Rpm) {
557+
packageName = 'explicit-permissions-test'
558+
from('src') {
559+
// Override executable script to be non-executable
560+
include 'script.sh'
561+
filePermissions {
562+
unix(0644)
563+
}
564+
}
565+
from('src') {
566+
// Override non-executable file to be executable
567+
include 'data.txt'
568+
filePermissions {
569+
unix(0755)
570+
}
571+
}
572+
}
573+
"""
574+
575+
when:
576+
runTasks('buildRpm')
577+
578+
then:
579+
def rpmFile = file('build/distributions/explicit-permissions-test-1.0.0.noarch.rpm')
580+
def scanFiles = Scanner.scan(rpmFile).files
581+
def scriptEntry = scanFiles.find { it.name == './script.sh' }
582+
def dataEntry = scanFiles.find { it.name == './data.txt' }
583+
584+
// Explicit permissions should override filesystem detection
585+
scriptEntry.permissions == 0644 // Explicitly set to non-executable
586+
dataEntry.permissions == 0755 // Explicitly set to executable
587+
}
532588
}

0 commit comments

Comments
 (0)